* [PATCH net] tls: refuse RX records after record-number exhaustion
@ 2026-10-13 0:27 Rishikesh Jethwani
2026-10-13 0:29 ` netdev-bot+sinfo
2026-10-13 9:38 ` Sabrina Dubroca
0 siblings, 2 replies; 4+ messages in thread
From: Rishikesh Jethwani @ 2026-10-13 0:27 UTC (permalink / raw)
To: Jakub Kicinski, Sabrina Dubroca, John Fastabend
Cc: David S . Miller, Eric Dumazet, Paolo Abeni, Simon Horman,
Jonathan Corbet, Shuah Khan, netdev, linux-doc, linux-kselftest,
linux-kernel, Rishikesh Jethwani
A receive socket records sequence-number exhaustion only in sk_err.
After processing record UINT64_MAX, it advances the counter to zero and
reports EBADMSG, but SO_ERROR or sock_error() can clear that error. A
parser-ready record also bypasses the socket-error check in the record
wait. The receiver can then accept a record authenticated for sequence
zero under the same exhausted key.
Getting there needs an initial record number installed close to the
limit, since 2^64 records cannot be exchanged in practice, and a peer
that sends the wrapped record, which the Linux TX path refuses to do.
Mark the RX key exhausted when its record number wraps, and refuse to
wait for or decrypt another record under it, or to replace the key.
Plaintext that is already decrypted can still be read. RFC 8446 lets a
sender retire a key with a KeyUpdate as its last record, so in that
case leave sk_err alone, report the pending key update as usual and
let the new TLS_RX key clear the mark.
Add fixed AES-GCM wire vectors and TLS 1.2/1.3 selftests for queued
wrapped records, SO_ERROR clearing, splice, peek, partial reads, TLS
1.3 key replacement and a KeyUpdate in the last record. Every case
first verifies the penultimate record succeeds. The vectors avoid a
userspace crypto dependency and the TX path's refusal to generate the
maximum-number record.
Fixes: c46234ebb4d1 ("tls: RX path for ktls")
Assisted-by: LLM
Signed-off-by: Rishikesh Jethwani <rjethwani@purestorage.com>
---
Documentation/networking/tls.rst | 17 ++
include/net/tls.h | 2 +
net/tls/tls.h | 19 ++-
net/tls/tls_main.c | 9 ++
net/tls/tls_sw.c | 19 ++-
tools/testing/selftests/net/tls.c | 258 +++++++++++++++++++++++++++++-
6 files changed, 315 insertions(+), 9 deletions(-)
diff --git a/Documentation/networking/tls.rst b/Documentation/networking/tls.rst
index 980c442d7161..4b3c7a5a0780 100644
--- a/Documentation/networking/tls.rst
+++ b/Documentation/networking/tls.rst
@@ -202,6 +202,23 @@ received without a cmsg buffer set.
recv will never return data from mixed types of TLS records.
+Receive record-number exhaustion
+--------------------------------
+
+The receive record number must not wrap under the same key. Once the
+record with the last 64-bit record number has been received, no further
+record is decrypted under that key: a receive that needs another record
+fails with ``EBADMSG``, even after ``SO_ERROR`` has been read. Plaintext
+that was already decrypted, such as the rest of a partially read or
+peeked final record, can still be read. This applies to both software
+and hardware receive offload.
+
+Unless the final record is a TLS 1.3 KeyUpdate, the condition is
+terminal: the socket error is also set to ``EBADMSG``, and a ``TLS_RX``
+key replacement fails with ``EBADMSG``. A KeyUpdate in the final record
+retires the key the usual way: receives fail with ``EKEYEXPIRED`` until a
+new ``TLS_RX`` key is installed, after which receiving continues.
+
TLS 1.3 Key Updates
-------------------
diff --git a/include/net/tls.h b/include/net/tls.h
index e57bef58851e..73f2d0f24e54 100644
--- a/include/net/tls.h
+++ b/include/net/tls.h
@@ -139,6 +139,8 @@ struct tls_sw_context_rx {
u8 zc_capable:1;
u8 reader_contended:1;
bool key_update_pending;
+ /* record numbers of this key are used up; cleared by a KeyUpdate */
+ bool rec_seq_exhausted;
struct tls_strparser strp;
diff --git a/net/tls/tls.h b/net/tls/tls.h
index 60a37bdaaa25..3b2af529d977 100644
--- a/net/tls/tls.h
+++ b/net/tls/tls.h
@@ -307,17 +307,26 @@ static inline void tls_bigint_subtract(unsigned char *seq, int n)
*p = cpu_to_be64(rcd_sn - n);
}
-static inline void
-tls_advance_record_sn(struct sock *sk, struct tls_prot_info *prot,
- struct cipher_context *ctx)
+/* Returns true if the record number wrapped */
+static inline bool
+__tls_advance_record_sn(struct tls_prot_info *prot, struct cipher_context *ctx)
{
- if (tls_bigint_increment(ctx->rec_seq, prot->rec_seq_size))
- tls_err_abort(sk, -EBADMSG);
+ bool overflow = tls_bigint_increment(ctx->rec_seq, prot->rec_seq_size);
if (prot->version != TLS_1_3_VERSION &&
prot->cipher_type != TLS_CIPHER_CHACHA20_POLY1305)
tls_bigint_increment(ctx->iv + prot->salt_size,
prot->iv_size);
+
+ return overflow;
+}
+
+static inline void
+tls_advance_record_sn(struct sock *sk, struct tls_prot_info *prot,
+ struct cipher_context *ctx)
+{
+ if (__tls_advance_record_sn(prot, ctx))
+ tls_err_abort(sk, -EBADMSG);
}
static inline void
diff --git a/net/tls/tls_main.c b/net/tls/tls_main.c
index fbb274287aa5..63bef0153182 100644
--- a/net/tls/tls_main.c
+++ b/net/tls/tls_main.c
@@ -730,6 +730,15 @@ static int do_tls_setsockopt_conf(struct sock *sk, sockptr_t optval,
conf = TLS_SW;
}
} else {
+ struct tls_sw_context_rx *rx_ctx = tls_sw_ctx_rx(ctx);
+
+ /* Only a KeyUpdate may follow the last record number */
+ if (update && rx_ctx->rec_seq_exhausted &&
+ !rx_ctx->key_update_pending) {
+ rc = -EBADMSG;
+ goto err_crypto_info;
+ }
+
rc = tls_set_device_offload_rx(sk, ctx);
conf = TLS_HW;
if (!rc) {
diff --git a/net/tls/tls_sw.c b/net/tls/tls_sw.c
index d1ad31986cf2..cd1590e1f20f 100644
--- a/net/tls/tls_sw.c
+++ b/net/tls/tls_sw.c
@@ -1104,6 +1104,10 @@ tls_rx_rec_wait(struct sock *sk, bool nonblock, bool released, bool has_copied)
if (unlikely(ctx->key_update_pending))
return -EKEYEXPIRED;
+ /* no record can follow the last one under this key */
+ if (unlikely(ctx->rec_seq_exhausted))
+ return -EBADMSG;
+
timeo = sock_rcvtimeo(sk, nonblock);
while (!tls_strp_msg_ready(ctx)) {
@@ -1566,6 +1570,7 @@ static int tls_rx_one_record(struct sock *sk, struct msghdr *msg,
{
struct tls_context *tls_ctx = tls_get_ctx(sk);
struct tls_prot_info *prot = &tls_ctx->prot_info;
+ struct tls_sw_context_rx *ctx = tls_sw_ctx_rx(tls_ctx);
struct strp_msg *rxm;
int err;
@@ -1580,9 +1585,18 @@ static int tls_rx_one_record(struct sock *sk, struct msghdr *msg,
rxm = strp_msg(darg->skb);
rxm->offset += prot->prepend_size;
rxm->full_len -= prot->overhead_size;
- tls_advance_record_sn(sk, prot, &tls_ctx->rx);
+ err = tls_check_pending_rekey(sk, tls_ctx, darg->skb);
+ if (__tls_advance_record_sn(prot, &tls_ctx->rx)) {
+ /* sk_err alone can't stop the next record, SO_ERROR clears
+ * it. A KeyUpdate in the last record properly retires the
+ * key, so don't treat that one as an error.
+ */
+ ctx->rec_seq_exhausted = true;
+ if (!ctx->key_update_pending)
+ tls_err_abort(sk, -EBADMSG);
+ }
- return tls_check_pending_rekey(sk, tls_ctx, darg->skb);
+ return err;
}
int decrypt_skb(struct sock *sk, struct scatterlist *sgout)
@@ -2518,6 +2532,7 @@ static void tls_finish_key_update(struct sock *sk, struct tls_context *tls_ctx)
struct tls_sw_context_rx *ctx = tls_ctx->priv_ctx_rx;
WRITE_ONCE(ctx->key_update_pending, false);
+ ctx->rec_seq_exhausted = false;
/* wake-up pre-existing poll() */
ctx->saved_data_ready(sk);
}
diff --git a/tools/testing/selftests/net/tls.c b/tools/testing/selftests/net/tls.c
index 9d3cd4fff062..f69c3a736b94 100644
--- a/tools/testing/selftests/net/tls.c
+++ b/tools/testing/selftests/net/tls.c
@@ -26,6 +26,7 @@
#define TLS_PAYLOAD_MAX_LEN 16384
#define TLS_HDR_LEN 5
#define SOL_TLS 282
+#define TLS_RECORD_TYPE_HANDSHAKE 0x16
static int fips_enabled;
@@ -300,6 +301,261 @@ TEST_F(tls_basic, recseq_wrap)
EXPECT_EQ(errno, EBADMSG);
}
+/* Fixed AES-GCM records avoid a dependency on a userspace crypto library.
+ * Each authenticates "test", with zero key and static IV, at sequence
+ * numbers UINT64_MAX - 1, UINT64_MAX and zero. TLS 1.2 uses the record
+ * number as its explicit IV; TLS 1.3 appends inner content type 23.
+ * The last record must never be accepted after counter exhaustion.
+ * tls_rx_recseq_ku_record is a TLS 1.3 KeyUpdate at UINT64_MAX.
+ */
+static const char tls_rx_recseq_ku_record[] =
+ "\x17\x03\x03\x00\x16\x49\xd1\xc7\x79\x45\x74\xd7"
+ "\x12\xe8\xbf\xe4\xaa\x70\x17\x58\x60\x66\xa0\xf9"
+ "\x5b\xac\x6a";
+
+FIXTURE(tls_rx_recseq)
+{
+ int fd, cfd;
+ bool notls;
+};
+
+FIXTURE_VARIANT(tls_rx_recseq)
+{
+ uint16_t tls_version;
+ size_t record_len;
+ const char *records[3];
+};
+
+FIXTURE_VARIANT_ADD(tls_rx_recseq, 12_aes_gcm)
+{
+ .tls_version = TLS_1_2_VERSION,
+ .record_len = 33,
+ .records = {
+ "\x17\x03\x03\x00\x1c\xff\xff\xff\xff\xff\xff\xff"
+ "\xfe\x62\x3c\xf3\xee\x4d\x18\x45\xf7\x62\x62\x2c"
+ "\x01\x9a\xfc\x47\x3d\x82\xb0\x9f\x7f",
+ "\x17\x03\x03\x00\x1c\xff\xff\xff\xff\xff\xff\xff"
+ "\xff\x25\xb4\xb4\x0c\xf7\x4c\xef\x86\xa2\xe1\x2d"
+ "\xa6\x31\xb5\x64\xf6\x21\x81\x7f\xad",
+ "\x17\x03\x03\x00\x1c\x00\x00\x00\x00\x00\x00\x00"
+ "\x00\x77\xed\xa9\xba\xbb\x29\xb7\x1a\x44\x35\x46"
+ "\x0f\x55\x94\x07\xdd\xc3\xe4\x91\x80",
+ },
+};
+
+FIXTURE_VARIANT_ADD(tls_rx_recseq, 13_aes_gcm)
+{
+ .tls_version = TLS_1_3_VERSION,
+ .record_len = 26,
+ .records = {
+ "\x17\x03\x03\x00\x15\x62\x3c\xf3\xee\xd7\x87\xd1"
+ "\x21\x46\x96\xc2\xfc\x59\x1c\x4c\x15\xec\x57\x6d"
+ "\xc8\x0a",
+ "\x17\x03\x03\x00\x15\x25\xb4\xb4\x0c\x52\x2e\x4d"
+ "\xcd\xf4\xec\xc2\x7e\xb8\x85\xed\xb7\x79\xf8\x39"
+ "\x68\xd6",
+ "\x17\x03\x03\x00\x15\x77\xed\xa9\xba\x77\x59\x33"
+ "\x88\x89\x13\x8d\x3c\xb4\x2e\xe0\x64\x72\xa5\x17"
+ "\x71\x45",
+ },
+};
+
+FIXTURE_SETUP(tls_rx_recseq)
+{
+ struct timeval timeout = { .tv_sec = 2 };
+ struct tls_crypto_info_keys key;
+ socklen_t len;
+ char buf[4];
+ int err;
+
+ ulp_sock_pair(_metadata, &self->fd, &self->cfd, &self->notls);
+ if (self->notls)
+ SKIP(return, "no TLS support");
+
+ ASSERT_EQ(setsockopt(self->cfd, SOL_SOCKET, SO_RCVTIMEO,
+ &timeout, sizeof(timeout)), 0);
+ tls_crypto_info_init(variant->tls_version, TLS_CIPHER_AES_GCM_128,
+ &key, 0);
+ memset(key.aes128.rec_seq, 0xff, sizeof(key.aes128.rec_seq));
+ key.aes128.rec_seq[sizeof(key.aes128.rec_seq) - 1]--;
+ ASSERT_EQ(setsockopt(self->cfd, SOL_TLS, TLS_RX, &key, key.len), 0);
+
+ /* The penultimate record must still advance normally. */
+ ASSERT_EQ(send(self->fd, variant->records[0], variant->record_len, 0),
+ variant->record_len);
+ ASSERT_EQ(recv(self->cfd, buf, sizeof(buf), MSG_WAITALL), sizeof(buf));
+ ASSERT_EQ(memcmp(buf, "test", sizeof(buf)), 0);
+ len = sizeof(err);
+ ASSERT_EQ(getsockopt(self->cfd, SOL_SOCKET, SO_ERROR, &err, &len), 0);
+ ASSERT_EQ(err, 0);
+};
+
+FIXTURE_TEARDOWN(tls_rx_recseq)
+{
+ close(self->fd);
+ close(self->cfd);
+};
+
+static void tls_clear_rx_error(struct __test_metadata *_metadata, int fd)
+{
+ socklen_t len;
+ int err;
+
+ len = sizeof(err);
+ ASSERT_EQ(getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &len), 0);
+ ASSERT_EQ(err, EBADMSG);
+ len = sizeof(err);
+ ASSERT_EQ(getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &len), 0);
+ ASSERT_EQ(err, 0);
+}
+
+TEST_F(tls_rx_recseq, queued)
+{
+ char records[80], buf[8];
+
+ memcpy(records, variant->records[1], variant->record_len);
+ memcpy(records + variant->record_len, variant->records[2],
+ variant->record_len);
+ ASSERT_EQ(send(self->fd, records, 2 * variant->record_len, 0),
+ 2 * variant->record_len);
+
+ /* Both records are queued, but only the final valid one can pass. */
+ ASSERT_EQ(recv(self->cfd, buf, sizeof(buf), MSG_WAITALL), 4);
+ EXPECT_EQ(memcmp(buf, "test", 4), 0);
+ tls_clear_rx_error(_metadata, self->cfd);
+ EXPECT_EQ(recv(self->cfd, buf, sizeof(buf), MSG_DONTWAIT), -1);
+ EXPECT_EQ(errno, EBADMSG);
+}
+
+TEST_F(tls_rx_recseq, so_error)
+{
+ char buf[4];
+
+ ASSERT_EQ(send(self->fd, variant->records[1], variant->record_len, 0),
+ variant->record_len);
+ ASSERT_EQ(recv(self->cfd, buf, sizeof(buf), MSG_WAITALL), sizeof(buf));
+ tls_clear_rx_error(_metadata, self->cfd);
+
+ ASSERT_EQ(send(self->fd, variant->records[2], variant->record_len, 0),
+ variant->record_len);
+ EXPECT_EQ(recv(self->cfd, buf, sizeof(buf), 0), -1);
+ EXPECT_EQ(errno, EBADMSG);
+ EXPECT_EQ(recv(self->cfd, buf, sizeof(buf), MSG_DONTWAIT), -1);
+ EXPECT_EQ(errno, EBADMSG);
+}
+
+TEST_F(tls_rx_recseq, splice)
+{
+ char buf[4];
+ int p[2];
+
+ ASSERT_EQ(send(self->fd, variant->records[1], variant->record_len, 0),
+ variant->record_len);
+ ASSERT_EQ(recv(self->cfd, buf, sizeof(buf), MSG_WAITALL), sizeof(buf));
+ tls_clear_rx_error(_metadata, self->cfd);
+
+ ASSERT_EQ(send(self->fd, variant->records[2], variant->record_len, 0),
+ variant->record_len);
+ ASSERT_EQ(pipe(p), 0);
+ EXPECT_EQ(splice(self->cfd, NULL, p[1], NULL, sizeof(buf), 0), -1);
+ EXPECT_EQ(errno, EBADMSG);
+ close(p[0]);
+ close(p[1]);
+}
+
+TEST_F(tls_rx_recseq, peek)
+{
+ char buf[4];
+
+ ASSERT_EQ(send(self->fd, variant->records[1], variant->record_len, 0),
+ variant->record_len);
+ ASSERT_EQ(recv(self->cfd, buf, sizeof(buf), MSG_PEEK), sizeof(buf));
+ tls_clear_rx_error(_metadata, self->cfd);
+
+ /* Plaintext decrypted before exhaustion can still be read. */
+ EXPECT_EQ(recv(self->cfd, buf, sizeof(buf), MSG_DONTWAIT), sizeof(buf));
+ EXPECT_EQ(memcmp(buf, "test", sizeof(buf)), 0);
+ EXPECT_EQ(recv(self->cfd, buf, sizeof(buf), MSG_DONTWAIT), -1);
+ EXPECT_EQ(errno, EBADMSG);
+}
+
+TEST_F(tls_rx_recseq, partial_read)
+{
+ char buf[4];
+
+ ASSERT_EQ(send(self->fd, variant->records[1], variant->record_len, 0),
+ variant->record_len);
+ ASSERT_EQ(recv(self->cfd, buf, 1, 0), 1);
+ tls_clear_rx_error(_metadata, self->cfd);
+
+ EXPECT_EQ(recv(self->cfd, buf, sizeof(buf), MSG_DONTWAIT), 3);
+ EXPECT_EQ(memcmp(buf, "est", 3), 0);
+ EXPECT_EQ(recv(self->cfd, buf, sizeof(buf), MSG_DONTWAIT), -1);
+ EXPECT_EQ(errno, EBADMSG);
+}
+
+/* Only a KeyUpdate may introduce a key after the last record number. */
+TEST_F(tls_rx_recseq, rekey)
+{
+ struct tls_crypto_info_keys key;
+ struct tls12_crypto_info_aes_gcm_128 before, after;
+ socklen_t len;
+ char buf[4];
+
+ if (variant->tls_version != TLS_1_3_VERSION)
+ SKIP(return, "rekey requires TLS 1.3");
+
+ ASSERT_EQ(send(self->fd, variant->records[1], variant->record_len, 0),
+ variant->record_len);
+ ASSERT_EQ(recv(self->cfd, buf, sizeof(buf), MSG_WAITALL), sizeof(buf));
+ tls_clear_rx_error(_metadata, self->cfd);
+
+ len = sizeof(before);
+ ASSERT_EQ(getsockopt(self->cfd, SOL_TLS, TLS_RX, &before, &len), 0);
+ tls_crypto_info_init(variant->tls_version, TLS_CIPHER_AES_GCM_128,
+ &key, 1);
+ EXPECT_EQ(setsockopt(self->cfd, SOL_TLS, TLS_RX, &key, key.len), -1);
+ EXPECT_EQ(errno, EBADMSG);
+ len = sizeof(after);
+ ASSERT_EQ(getsockopt(self->cfd, SOL_TLS, TLS_RX, &after, &len), 0);
+ EXPECT_EQ(memcmp(&before, &after, sizeof(before)), 0);
+}
+
+/* A KeyUpdate in the last record retires the key without an error. */
+TEST_F(tls_rx_recseq, keyupdate)
+{
+ size_t len = sizeof(tls_rx_recseq_ku_record) - 1;
+ struct tls_crypto_info_keys key;
+ socklen_t optlen;
+ char buf[16];
+ int err;
+
+ if (variant->tls_version != TLS_1_3_VERSION)
+ SKIP(return, "KeyUpdate requires TLS 1.3");
+
+ ASSERT_EQ(send(self->fd, tls_rx_recseq_ku_record, len, 0), len);
+ EXPECT_EQ(tls_recv_cmsg(_metadata, self->cfd, TLS_RECORD_TYPE_HANDSHAKE,
+ buf, sizeof(buf), 0), 5);
+ EXPECT_EQ(memcmp(buf, "\x18\x00\x00\x01\x00", 5), 0);
+ EXPECT_EQ(recv(self->cfd, buf, sizeof(buf), MSG_DONTWAIT), -1);
+ EXPECT_EQ(errno, EKEYEXPIRED);
+ optlen = sizeof(err);
+ ASSERT_EQ(getsockopt(self->cfd, SOL_SOCKET, SO_ERROR, &err, &optlen), 0);
+ EXPECT_EQ(err, 0);
+
+ /* The new key resets the record number, so receiving continues. */
+ tls_crypto_info_init(variant->tls_version, TLS_CIPHER_AES_GCM_128,
+ &key, 1);
+ ASSERT_EQ(setsockopt(self->cfd, SOL_TLS, TLS_RX, &key, key.len), 0);
+ ASSERT_EQ(setsockopt(self->fd, SOL_TLS, TLS_TX, &key, key.len), 0);
+ ASSERT_EQ(send(self->fd, "test", 4, 0), 4);
+ ASSERT_EQ(recv(self->cfd, buf, 4, MSG_WAITALL), 4);
+ EXPECT_EQ(memcmp(buf, "test", 4), 0);
+ optlen = sizeof(err);
+ ASSERT_EQ(getsockopt(self->cfd, SOL_SOCKET, SO_ERROR, &err, &optlen), 0);
+ EXPECT_EQ(err, 0);
+}
+
FIXTURE(tls)
{
int fd, cfd;
@@ -1839,8 +2095,6 @@ TEST_F(tls, recv_efault)
EXPECT_EQ(memcmp(rec2, recv_mem + 9, ret - 9), 0);
}
-#define TLS_RECORD_TYPE_HANDSHAKE 0x16
-
TEST_F(tls_basic, recvmsg_nopad_retry_iov)
{
char payload[32];
base-commit: 8e2fbe0e75d38690d551456658194c632933cb57
--
2.50.1
^ permalink raw reply related [flat|nested] 4+ messages in thread* Re: [PATCH net] tls: refuse RX records after record-number exhaustion
2026-10-13 0:27 [PATCH net] tls: refuse RX records after record-number exhaustion Rishikesh Jethwani
@ 2026-10-13 0:29 ` netdev-bot+sinfo
2026-10-13 9:38 ` Sabrina Dubroca
1 sibling, 0 replies; 4+ messages in thread
From: netdev-bot+sinfo @ 2026-10-13 0:29 UTC (permalink / raw)
To: Rishikesh Jethwani
Cc: Jakub Kicinski, Sabrina Dubroca, John Fastabend, David S . Miller,
Eric Dumazet, Paolo Abeni, Simon Horman, Jonathan Corbet,
Shuah Khan, netdev, linux-doc, linux-kselftest, linux-kernel
Hi!
This is an automated message. This series looks like a fix, but its
commit messages seem to be missing some information:
- How the issue was discovered, e.g. hit in production, hit during
development, syzbot report, manual code inspection, LLM or static
analysis tool scan.
Please do not repost the series just to address the above. Instead,
reply to this email with the missing information, so that reviewers
can take it into account. If the series needs another revision for
other reasons, please include the information in the commit messages
then.
The evaluation is done by an LLM so it may be wrong, if you think
that is the case please reply and explain.
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH net] tls: refuse RX records after record-number exhaustion
2026-10-13 0:27 [PATCH net] tls: refuse RX records after record-number exhaustion Rishikesh Jethwani
2026-10-13 0:29 ` netdev-bot+sinfo
@ 2026-10-13 9:38 ` Sabrina Dubroca
2026-10-13 17:55 ` Rishikesh Jethwani
1 sibling, 1 reply; 4+ messages in thread
From: Sabrina Dubroca @ 2026-10-13 9:38 UTC (permalink / raw)
To: Rishikesh Jethwani
Cc: Jakub Kicinski, John Fastabend, David S . Miller, Eric Dumazet,
Paolo Abeni, Simon Horman, Jonathan Corbet, Shuah Khan, netdev,
linux-doc, linux-kselftest, linux-kernel
2026-10-12, 18:27:01 -0600, Rishikesh Jethwani wrote:
> A receive socket records sequence-number exhaustion only in sk_err.
> After processing record UINT64_MAX, it advances the counter to zero and
> reports EBADMSG, but SO_ERROR or sock_error() can clear that error.
I vaguely remember a discussion around making some fatal errors more
permanent (ie not use sk_err).
> parser-ready record also bypasses the socket-error check in the record
> wait. The receiver can then accept a record authenticated for sequence
> zero under the same exhausted key.
I guess, but what's the problem with wrapping as long as we don't
reuse the same record number?
> Getting there needs an initial record number installed close to the
> limit, since 2^64 records cannot be exchanged in practice, and a peer
> that sends the wrapped record, which the Linux TX path refuses to do.
>
> Mark the RX key exhausted when its record number wraps, and refuse to
> wait for or decrypt another record under it, or to replace the key.
> Plaintext that is already decrypted can still be read. RFC 8446 lets a
> sender retire a key with a KeyUpdate as its last record, so in that
> case leave sk_err alone, report the pending key update as usual and
> let the new TLS_RX key clear the mark.
>
> Add fixed AES-GCM wire vectors and TLS 1.2/1.3 selftests for queued
> wrapped records, SO_ERROR clearing, splice, peek, partial reads, TLS
> 1.3 key replacement and a KeyUpdate in the last record. Every case
> first verifies the penultimate record succeeds. The vectors avoid a
> userspace crypto dependency and the TX path's refusal to generate the
> maximum-number record.
For 1.3, just do a "silent rekey" (change the key but don't send a
keyupdate) on the TX side to record number 0 with the same key? And it
should be enough to test this with just 1.3.
> diff --git a/net/tls/tls_main.c b/net/tls/tls_main.c
> index fbb274287aa5..63bef0153182 100644
> --- a/net/tls/tls_main.c
> +++ b/net/tls/tls_main.c
> @@ -730,6 +730,15 @@ static int do_tls_setsockopt_conf(struct sock *sk, sockptr_t optval,
> conf = TLS_SW;
> }
> } else {
> + struct tls_sw_context_rx *rx_ctx = tls_sw_ctx_rx(ctx);
> +
> + /* Only a KeyUpdate may follow the last record number */
> + if (update && rx_ctx->rec_seq_exhausted &&
> + !rx_ctx->key_update_pending) {
I'm really confused by this.
> + rc = -EBADMSG;
> + goto err_crypto_info;
> + }
> +
--
Sabrina
^ permalink raw reply [flat|nested] 4+ messages in thread* Re: [PATCH net] tls: refuse RX records after record-number exhaustion
2026-10-13 9:38 ` Sabrina Dubroca
@ 2026-10-13 17:55 ` Rishikesh Jethwani
0 siblings, 0 replies; 4+ messages in thread
From: Rishikesh Jethwani @ 2026-10-13 17:55 UTC (permalink / raw)
To: Sabrina Dubroca
Cc: Jakub Kicinski, John Fastabend, David S . Miller, Eric Dumazet,
Paolo Abeni, Simon Horman, Jonathan Corbet, Shuah Khan, netdev,
linux-doc, linux-kselftest, linux-kernel
On Tue, Oct 13, 2026 at 2:38 AM Sabrina Dubroca <sd@queasysnail.net> wrote:
>
> 2026-10-12, 18:27:01 -0600, Rishikesh Jethwani wrote:
> > A receive socket records sequence-number exhaustion only in sk_err.
> > After processing record UINT64_MAX, it advances the counter to zero and
> > reports EBADMSG, but SO_ERROR or sock_error() can clear that error.
>
> I vaguely remember a discussion around making some fatal errors more
> permanent (ie not use sk_err).
Do you have a pointer to that thread? If there's a preferred way to make
these errors sticky, I'd rather use it than add a one-off flag.
> > parser-ready record also bypasses the socket-error check in the record
> > wait. The receiver can then accept a record authenticated for sequence
> > zero under the same exhausted key.
>
> I guess, but what's the problem with wrapping as long as we don't
> reuse the same record number?
You're right, in practice there isn't one. The only way to get there is
for userspace to install a rec_seq close to the limit, so sequence 0 was
never used under that key and nothing can be replayed. Starting there
already breaks RFC 8446 5.3, which requires the first record under a
key to use sequence number 0.
What's left is consistency. Today the wrap gives at most one EBADMSG,
and none if the next record has already been parsed. After that recv
continues at record 0, which is neither rekeying nor terminating. TX
behaves the same way: the UINT64_MAX record is queued, and it goes out
once SO_ERROR is cleared.
So I'll drop the security framing and the Fixes tag. I can either drop
the patch, or send a minimal version for net/net-next that makes the wrap
error stick on both RX and TX, without touching the rekey path. Which
would you prefer?
> > Getting there needs an initial record number installed close to the
> > limit, since 2^64 records cannot be exchanged in practice, and a peer
> > that sends the wrapped record, which the Linux TX path refuses to do.
> >
> > Mark the RX key exhausted when its record number wraps, and refuse to
> > wait for or decrypt another record under it, or to replace the key.
> > Plaintext that is already decrypted can still be read. RFC 8446 lets a
> > sender retire a key with a KeyUpdate as its last record, so in that
> > case leave sk_err alone, report the pending key update as usual and
> > let the new TLS_RX key clear the mark.
> >
> > Add fixed AES-GCM wire vectors and TLS 1.2/1.3 selftests for queued
> > wrapped records, SO_ERROR clearing, splice, peek, partial reads, TLS
> > 1.3 key replacement and a KeyUpdate in the last record. Every case
> > first verifies the penultimate record succeeds. The vectors avoid a
> > userspace crypto dependency and the TX path's refusal to generate the
> > maximum-number record.
>
> For 1.3, just do a "silent rekey" (change the key but don't send a
> keyupdate) on the TX side to record number 0 with the same key? And it
> should be enough to test this with just 1.3.
Agreed, that gets rid of the fixed vectors. Since 1.3 only uses the
record number in the nonce, TX can also produce the UINT64_MAX record
cleanly, using rec_seq 0 and the IV XORed with UINT64_MAX. If the patch
stays, the test will be 1.3-only and built that way.
> > diff --git a/net/tls/tls_main.c b/net/tls/tls_main.c
> > index fbb274287aa5..63bef0153182 100644
> > --- a/net/tls/tls_main.c
> > +++ b/net/tls/tls_main.c
> > @@ -730,6 +730,15 @@ static int do_tls_setsockopt_conf(struct sock *sk, sockptr_t optval,
> > conf = TLS_SW;
> > }
> > } else {
> > + struct tls_sw_context_rx *rx_ctx = tls_sw_ctx_rx(ctx);
> > +
> > + /* Only a KeyUpdate may follow the last record number */
> > + if (update && rx_ctx->rec_seq_exhausted &&
> > + !rx_ctx->key_update_pending) {
>
> I'm really confused by this.
It refused a TLS_RX rekey after a
a wrap that didn't include a KeyUpdate. A new key is harmless, as you
say, so I'll remove it.
> > + rc = -EBADMSG;
> > + goto err_crypto_info;
> > + }
> > +
>
> --
> Sabrina
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-10-13 17:55 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-13 0:27 [PATCH net] tls: refuse RX records after record-number exhaustion Rishikesh Jethwani
2026-10-13 0:29 ` netdev-bot+sinfo
2026-10-13 9:38 ` Sabrina Dubroca
2026-10-13 17:55 ` Rishikesh Jethwani
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.