git: 54372e3b56b7 - releng/15.1 - ktls CBC decrypt: Only increment iovec index when an entry is used

From: Mark Johnston <markj_at_FreeBSD.org>
Date: Tue, 30 Jun 2026 17:22:06 UTC
The branch releng/15.1 has been updated by markj:

URL: https://cgit.FreeBSD.org/src/commit/?id=54372e3b56b7b7b8946705a8116cc3ed540d511d

commit 54372e3b56b7b7b8946705a8116cc3ed540d511d
Author:     John Baldwin <jhb@FreeBSD.org>
AuthorDate: 2026-06-24 01:14:11 +0000
Commit:     Mark Johnston <markj@FreeBSD.org>
CommitDate: 2026-06-29 19:16:41 +0000

    ktls CBC decrypt: Only increment iovec index when an entry is used
    
    If an mbuf in the chain was skipped because it only contained bytes
    from the header, the iovec index ('i') was incremented even though the
    entry was not populated.  Only increment 'i' when an iovec entry is
    consumed.
    
    Add a new type of KTLS receive test which writes a single TLS record
    via two separate write(2) calls over a TCP_NODELAY socket to trigger
    a split in the mbuf chain in the kernel.  Test various split locations
    including after the "plain" TLS header (5 bytes), after the full TLS
    header, in the middle of the data payload, just before the start of
    the trailer, and in the middle of the trailer.  These tests are also
    run against all supported ciphers, not just CBC.  The 'header' test
    for CBC ciphersuites was able to trigger the bug.
    
    Approved by:    so
    Security:       FreeBSD-SA-26:46.ktls
    Security:       CVE-2026-49423
    Sponsored by:   Chelsio Communications
---
 sys/opencrypto/ktls_ocf.c  |   3 +-
 tests/sys/kern/ktls_test.c | 110 ++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 110 insertions(+), 3 deletions(-)

diff --git a/sys/opencrypto/ktls_ocf.c b/sys/opencrypto/ktls_ocf.c
index ece509f5f969..e61f607622b0 100644
--- a/sys/opencrypto/ktls_ocf.c
+++ b/sys/opencrypto/ktls_ocf.c
@@ -500,13 +500,14 @@ ktls_ocf_tls_cbc_decrypt(struct ktls_session *tls,
 	iov[0].iov_base = &ad;
 	iov[0].iov_len = sizeof(ad);
 	skip = sizeof(*hdr) + AES_BLOCK_LEN;
-	for (i = 1, n = m; n != NULL; i++, n = n->m_next) {
+	for (i = 1, n = m; n != NULL; n = n->m_next) {
 		if (n->m_len < skip) {
 			skip -= n->m_len;
 			continue;
 		}
 		iov[i].iov_base = mtod(n, char *) + skip;
 		iov[i].iov_len = n->m_len - skip;
+		i++;
 		skip = 0;
 	}
 	uio.uio_iov = iov;
diff --git a/tests/sys/kern/ktls_test.c b/tests/sys/kern/ktls_test.c
index 3970083e7f72..7003b86fb4e2 100644
--- a/tests/sys/kern/ktls_test.c
+++ b/tests/sys/kern/ktls_test.c
@@ -303,6 +303,15 @@ fd_set_blocking(int fd)
 	ATF_REQUIRE(fcntl(fd, F_SETFL, flags) != -1);
 }
 
+static void
+tcp_nodelay(int fd)
+{
+	int nodelay = 1;
+
+	ATF_REQUIRE(setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &nodelay,
+	    sizeof(nodelay)) == 0);
+}
+
 static bool
 cbc_crypt(const EVP_CIPHER *cipher, const char *key, const char *iv,
     const char *input, char *output, size_t size, int enc)
@@ -1920,6 +1929,55 @@ test_ktls_receive_bad_size(const atf_tc_t *tc, struct tls_enable *en,
 	close_sockets_ignore_errors(sockets);
 }
 
+static void
+test_ktls_receive_split_record(const atf_tc_t *tc, struct tls_enable *en,
+    uint64_t seqno, size_t len, size_t first_len)
+{
+	char *plaintext, *received, *outbuf;
+	size_t outbuf_cap, outbuf_len;
+	ssize_t rv;
+	int sockets[2];
+
+	ATF_REQUIRE(len <= TLS_MAX_MSG_SIZE_V10_2);
+
+	plaintext = alloc_buffer(len);
+	received = malloc(len);
+	outbuf_cap = tls_header_len(en) + len + tls_trailer_len(en);
+	outbuf = malloc(outbuf_cap);
+
+	ATF_REQUIRE_MSG(open_sockets(tc, sockets), "failed to create sockets");
+
+	ATF_REQUIRE(setsockopt(sockets[0], IPPROTO_TCP, TCP_RXTLS_ENABLE, en,
+	    sizeof(*en)) == 0);
+	check_tls_mode(tc, sockets[0], TCP_RXTLS_MODE);
+
+	fd_set_blocking(sockets[0]);
+	fd_set_blocking(sockets[1]);
+
+	outbuf_len = encrypt_tls_record(tc, en, TLS_RLTYPE_APP, seqno,
+	    plaintext, len, outbuf, outbuf_cap, 0);
+	ATF_REQUIRE(first_len < outbuf_len);
+
+	tcp_nodelay(sockets[1]);
+	rv = write(sockets[1], outbuf, first_len);
+	ATF_REQUIRE_INTEQ((ssize_t)(first_len), rv);
+
+	rv = write(sockets[1], outbuf + first_len, outbuf_len - first_len);
+	ATF_REQUIRE_INTEQ((ssize_t)(outbuf_len - first_len), rv);
+
+	rv = ktls_receive_tls_record(en, sockets[0], TLS_RLTYPE_APP, received,
+	    len);
+	ATF_REQUIRE_INTEQ((ssize_t)len, rv);
+
+	ATF_REQUIRE(memcmp(plaintext, received, len) == 0);
+
+	free(outbuf);
+	free(received);
+	free(plaintext);
+
+	close_sockets(sockets);
+}
+
 #define	TLS_10_TESTS(M)							\
 	M(aes128_cbc_1_0_sha1, CRYPTO_AES_CBC, 128 / 8,			\
 	    CRYPTO_SHA1_HMAC, TLS_MINOR_VER_ZERO)			\
@@ -2360,6 +2418,26 @@ ATF_TC_BODY(ktls_receive_##cipher_name##_##name, tc)			\
 	    auth_alg, minor, name)					\
 	ATF_TP_ADD_TC(tp, ktls_receive_##cipher_name##_##name);
 
+#define GEN_RECEIVE_SPLIT_RECORD_TEST(cipher_name, cipher_alg,		\
+	    key_size, auth_alg, minor, name, len, first_len)		\
+ATF_TC_WITHOUT_HEAD(ktls_receive_##cipher_name##_split_##name);		\
+ATF_TC_BODY(ktls_receive_##cipher_name##_split_##name, tc)		\
+{									\
+	struct tls_enable en;						\
+	uint64_t seqno;							\
+									\
+	ATF_REQUIRE_KTLS();						\
+	seqno = random();						\
+	build_tls_enable(tc, cipher_alg, key_size, auth_alg, minor,	\
+	    seqno, &en);						\
+	test_ktls_receive_split_record(tc, &en, seqno, len, first_len);	\
+	free_tls_enable(&en);						\
+}
+
+#define ADD_RECEIVE_SPLIT_RECORD_TEST(cipher_name, cipher_alg,		\
+	    key_size, auth_alg, minor, name)				\
+	ATF_TP_ADD_TC(tp, ktls_receive_##cipher_name##_split_##name);
+
 #define GEN_RECEIVE_TESTS(cipher_name, cipher_alg, key_size, auth_alg,	\
 	    minor)							\
 	GEN_RECEIVE_APP_DATA_TEST(cipher_name, cipher_alg, key_size,	\
@@ -2381,7 +2459,22 @@ ATF_TC_BODY(ktls_receive_##cipher_name##_##name, tc)			\
 	    tls_minimum_record_payload(&en) - 1)			\
 	GEN_RECEIVE_BAD_SIZE_TEST(cipher_name, cipher_alg, key_size,	\
 	    auth_alg, minor, oversized_record,				\
-	    TLS_MAX_MSG_SIZE_V10_2 * 2)
+	    TLS_MAX_MSG_SIZE_V10_2 * 2)					\
+	GEN_RECEIVE_SPLIT_RECORD_TEST(cipher_name, cipher_alg,		\
+	    key_size, auth_alg, minor, header, 64,			\
+	    sizeof(struct tls_record_layer));				\
+	GEN_RECEIVE_SPLIT_RECORD_TEST(cipher_name, cipher_alg,		\
+	    key_size, auth_alg, minor, full_header, 64,			\
+	    tls_header_len(&en));					\
+	GEN_RECEIVE_SPLIT_RECORD_TEST(cipher_name, cipher_alg,		\
+	    key_size, auth_alg, minor, half, 64,			\
+	    tls_header_len(&en) + 32);					\
+	GEN_RECEIVE_SPLIT_RECORD_TEST(cipher_name, cipher_alg,		\
+	    key_size, auth_alg, minor, trailer_start, 64,		\
+	    tls_header_len(&en) + 64);					\
+	GEN_RECEIVE_SPLIT_RECORD_TEST(cipher_name, cipher_alg,		\
+	    key_size, auth_alg, minor, trailer_middle, 64,		\
+	    tls_header_len(&en) + 64 + tls_trailer_len(&en) / 2);
 
 #define ADD_RECEIVE_TESTS(cipher_name, cipher_alg, key_size, auth_alg,	\
 	    minor)							\
@@ -2402,7 +2495,17 @@ ATF_TC_BODY(ktls_receive_##cipher_name##_##name, tc)			\
 	ADD_RECEIVE_BAD_SIZE_TEST(cipher_name, cipher_alg, key_size,	\
 	    auth_alg, minor, small_record)				\
 	ADD_RECEIVE_BAD_SIZE_TEST(cipher_name, cipher_alg, key_size,	\
-	    auth_alg, minor, oversized_record)
+	    auth_alg, minor, oversized_record)				\
+	ADD_RECEIVE_SPLIT_RECORD_TEST(cipher_name, cipher_alg,		\
+	    key_size, auth_alg, minor, header)				\
+	ADD_RECEIVE_SPLIT_RECORD_TEST(cipher_name, cipher_alg,		\
+	    key_size, auth_alg, minor, full_header)			\
+	ADD_RECEIVE_SPLIT_RECORD_TEST(cipher_name, cipher_alg,		\
+	    key_size, auth_alg, minor, half)				\
+	ADD_RECEIVE_SPLIT_RECORD_TEST(cipher_name, cipher_alg,		\
+	    key_size, auth_alg, minor, trailer_start)			\
+	ADD_RECEIVE_SPLIT_RECORD_TEST(cipher_name, cipher_alg,		\
+	    key_size, auth_alg, minor, trailer_middle)			\
 
 /*
  * For each supported cipher suite, run several receive tests:
@@ -2425,6 +2528,9 @@ ATF_TC_BODY(ktls_receive_##cipher_name##_##name, tc)			\
  *   size
  *
  * - a test with an oversized TLS record
+ *
+ * - tests of a single record whose data is split across two writes,
+ *   with each test using a different split point
  */
 AES_CBC_NONZERO_TESTS(GEN_RECEIVE_TESTS);
 AES_GCM_TESTS(GEN_RECEIVE_TESTS);