From: Jakub Kicinski <kuba@kernel.org>
To: davem@davemloft.net
Cc: netdev@vger.kernel.org, edumazet@google.com, pabeni@redhat.com,
andrew+netdev@lunn.ch, horms@kernel.org, sd@queasysnail.net,
john.fastabend@gmail.com, shuah@kernel.org, cel@kernel.org,
linux-kselftest@vger.kernel.org, Jakub Kicinski <kuba@kernel.org>
Subject: [PATCH net v2 4/5] selftests: tls: Extend coverage for zero-length records
Date: Wed, 11 Nov 2026 10:55:21 -0800 [thread overview]
Message-ID: <20261111185522.2859004-5-kuba@kernel.org> (raw)
In-Reply-To: <20261111185522.2859004-1-kuba@kernel.org>
From: Chuck Lever <cel@kernel.org>
The zero_len fixture injects raw pre-encrypted records over a
socket carrying a TLS_RX key only, and some of those records are
zero-length application_data records. Every variant reads the
records back with recvmsg() without MSG_PEEK, so neither splice nor
MSG_PEEK is tested against an empty record.
Add zero_len_splice and zero_len_peek. A zero return from splice()
means EOF, so zero_len_splice checks that a run of empty data
records does not end a live connection. zero_len_peek checks that a
peek reaches the payload behind a run of empty records and leaves
the payload in place for the read that follows. When a payload does
not follow the run, the peek must leave the socket unreadable. An
unfixed kernel returns 0 from splice(), and after the peek poll()
reports the socket readable.
Signed-off-by: Chuck Lever <cel@kernel.org>
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
v2:
- bring back the poll() check after a peek at only empty records (Chuck)
v1: https://lore.kernel.org/20261109175836.1246840-5-kuba@kernel.org
---
tools/testing/selftests/net/tls.c | 226 +++++++++++++++++++++++++++---
1 file changed, 209 insertions(+), 17 deletions(-)
diff --git a/tools/testing/selftests/net/tls.c b/tools/testing/selftests/net/tls.c
index ca47576220d3..c505508afde5 100644
--- a/tools/testing/selftests/net/tls.c
+++ b/tools/testing/selftests/net/tls.c
@@ -2580,6 +2580,42 @@ static const struct raw_rec id2_data_l0 = {
},
};
+static void zero_len_sock_pair(struct __test_metadata *_metadata,
+ int *fd, int *cfd, bool *notls)
+{
+ struct tls_crypto_info_keys tls12;
+ int ret;
+
+ tls_crypto_info_init(TLS_1_2_VERSION, TLS_CIPHER_AES_CCM_128,
+ &tls12, 0);
+
+ ulp_sock_pair(_metadata, fd, cfd, notls);
+ if (*notls)
+ return;
+
+ /* fd gets no keys, since these fixtures send raw records over it */
+ ret = setsockopt(*cfd, SOL_TLS, TLS_RX, &tls12, tls12.len);
+ ASSERT_EQ(ret, 0);
+}
+
+/* Send a variant's records and return the last one carrying payload */
+static const struct raw_rec *
+zero_len_send_recs(struct __test_metadata *_metadata, int fd,
+ const struct raw_rec *const *recs)
+{
+ const struct raw_rec *payload = NULL;
+ int i;
+
+ for (i = 0; i < 4 && recs[i]; i++) {
+ EXPECT_EQ(send(fd, recs[i]->cipher_data, recs[i]->cipher_len, 0),
+ recs[i]->cipher_len);
+ if (recs[i]->plain_len)
+ payload = recs[i];
+ }
+
+ return payload;
+}
+
FIXTURE(zero_len)
{
int fd, cfd;
@@ -2642,19 +2678,7 @@ FIXTURE_VARIANT_ADD(zero_len, data_0data_0data)
FIXTURE_SETUP(zero_len)
{
- struct tls_crypto_info_keys tls12;
- int ret;
-
- tls_crypto_info_init(TLS_1_2_VERSION, TLS_CIPHER_AES_CCM_128,
- &tls12, 0);
-
- ulp_sock_pair(_metadata, &self->fd, &self->cfd, &self->notls);
- if (self->notls)
- return;
-
- /* Don't install keys on fd, we'll send raw records */
- ret = setsockopt(self->cfd, SOL_TLS, TLS_RX, &tls12, tls12.len);
- ASSERT_EQ(ret, 0);
+ zero_len_sock_pair(_metadata, &self->fd, &self->cfd, &self->notls);
}
FIXTURE_TEARDOWN(zero_len)
@@ -2673,10 +2697,7 @@ TEST_F(zero_len, test)
if (self->notls)
SKIP(return, "no TLS support");
- for (i = 0; i < 4 && variant->recs[i]; i++)
- EXPECT_EQ(send(self->fd, variant->recs[i]->cipher_data,
- variant->recs[i]->cipher_len, 0),
- variant->recs[i]->cipher_len);
+ zero_len_send_recs(_metadata, self->fd, variant->recs);
rec = &variant->recs[0];
rec_off = 0;
@@ -2702,6 +2723,177 @@ TEST_F(zero_len, test)
}
};
+FIXTURE(zero_len_peek)
+{
+ int fd, cfd;
+ bool notls;
+};
+
+FIXTURE_VARIANT(zero_len_peek)
+{
+ const struct raw_rec *recs[4];
+ ssize_t peek_ret;
+};
+
+FIXTURE_VARIANT_ADD(zero_len_peek, 0data_0data_data)
+{
+ .recs = { &id0_data_l0, &id1_data_l0, &id2_data_l11, },
+ .peek_ret = 11,
+};
+
+FIXTURE_VARIANT_ADD(zero_len_peek, 0data_0data_0data)
+{
+ .recs = { &id0_data_l0, &id1_data_l0, &id2_data_l0, },
+ .peek_ret = -EAGAIN,
+};
+
+FIXTURE_SETUP(zero_len_peek)
+{
+ zero_len_sock_pair(_metadata, &self->fd, &self->cfd, &self->notls);
+}
+
+FIXTURE_TEARDOWN(zero_len_peek)
+{
+ close(self->fd);
+ close(self->cfd);
+}
+
+/* Peeking past a run of empty data records must reach the payload
+ * behind them, and a run with no payload behind it must report EAGAIN
+ * and leave no record queued.
+ */
+TEST_F(zero_len_peek, test)
+{
+ struct pollfd pfd = { .fd = self->cfd, .events = POLLIN };
+ const struct raw_rec *payload;
+ unsigned char buf[128];
+ ssize_t ret;
+
+ if (self->notls)
+ SKIP(return, "no TLS support");
+
+ payload = zero_len_send_recs(_metadata, self->fd, variant->recs);
+
+ if (variant->peek_ret < 0) {
+ ret = recv(self->cfd, buf, sizeof(buf),
+ MSG_DONTWAIT | MSG_PEEK);
+ EXPECT_EQ(ret, -1);
+ EXPECT_EQ(errno, -variant->peek_ret);
+
+ /* A record left on rx_list keeps the socket readable */
+ EXPECT_EQ(poll(&pfd, 1, 0), 0);
+ return;
+ }
+
+ ret = recv(self->cfd, buf, sizeof(buf), MSG_DONTWAIT | MSG_PEEK);
+ EXPECT_EQ(ret, variant->peek_ret);
+ if (ret == variant->peek_ret)
+ EXPECT_EQ(memcmp(buf, payload->plain_data,
+ variant->peek_ret), 0);
+
+ ret = recv(self->cfd, buf, sizeof(buf), MSG_DONTWAIT);
+ EXPECT_EQ(ret, variant->peek_ret);
+ if (ret == variant->peek_ret)
+ EXPECT_EQ(memcmp(buf, payload->plain_data,
+ variant->peek_ret), 0);
+
+ ret = recv(self->cfd, buf, sizeof(buf), MSG_DONTWAIT);
+ EXPECT_EQ(ret, -1);
+ EXPECT_EQ(errno, EAGAIN);
+}
+
+FIXTURE(zero_len_splice)
+{
+ int fd, cfd;
+ bool notls;
+};
+
+FIXTURE_VARIANT(zero_len_splice)
+{
+ const struct raw_rec *recs[4];
+ ssize_t splice_ret;
+};
+
+FIXTURE_VARIANT_ADD(zero_len_splice, 0data_data)
+{
+ .recs = { &id0_data_l0, &id1_data_l11, },
+ .splice_ret = 11,
+};
+
+FIXTURE_VARIANT_ADD(zero_len_splice, 0data_0data_data)
+{
+ .recs = { &id0_data_l0, &id1_data_l0, &id2_data_l11, },
+ .splice_ret = 11,
+};
+
+FIXTURE_VARIANT_ADD(zero_len_splice, 0data_0data_0data)
+{
+ .recs = { &id0_data_l0, &id1_data_l0, &id2_data_l0, },
+ .splice_ret = -EAGAIN,
+};
+
+FIXTURE_VARIANT_ADD(zero_len_splice, 0data_0ctrl)
+{
+ .recs = { &id0_data_l0, &id1_ctrl_l0, },
+ .splice_ret = -EINVAL,
+};
+
+FIXTURE_SETUP(zero_len_splice)
+{
+ zero_len_sock_pair(_metadata, &self->fd, &self->cfd, &self->notls);
+}
+
+FIXTURE_TEARDOWN(zero_len_splice)
+{
+ close(self->fd);
+ close(self->cfd);
+}
+
+/* Splicing must skip a run of empty data records to reach the payload
+ * behind it, since a zero-byte splice reads as EOF. Splice reports
+ * EAGAIN for a run with no payload behind it, and EINVAL for a control
+ * record behind the run.
+ */
+TEST_F(zero_len_splice, test)
+{
+ struct pollfd pfd = { .fd = self->cfd, .events = POLLIN };
+ const struct raw_rec *payload;
+ unsigned char buf[128];
+ ssize_t ret;
+ int p[2];
+
+ if (self->notls)
+ SKIP(return, "no TLS support");
+
+ ASSERT_GE(pipe(p), 0);
+
+ payload = zero_len_send_recs(_metadata, self->fd, variant->recs);
+
+ if (variant->splice_ret < 0) {
+ ret = splice(self->cfd, NULL, p[1], NULL, sizeof(buf),
+ SPLICE_F_NONBLOCK);
+ EXPECT_EQ(ret, -1);
+ EXPECT_EQ(errno, -variant->splice_ret);
+ } else {
+ /* ASSERT so a zero return stops the test before the read
+ * below blocks until the harness timeout.
+ */
+ ASSERT_EQ(splice(self->cfd, NULL, p[1], NULL, sizeof(buf),
+ SPLICE_F_NONBLOCK), variant->splice_ret);
+ ret = read(p[0], buf, sizeof(buf));
+ EXPECT_EQ(ret, variant->splice_ret);
+ if (ret == variant->splice_ret)
+ EXPECT_EQ(memcmp(buf, payload->plain_data,
+ variant->splice_ret), 0);
+
+ /* The empty records were consumed, not left queued */
+ EXPECT_EQ(poll(&pfd, 1, 0), 0);
+ }
+
+ close(p[0]);
+ close(p[1]);
+}
+
FIXTURE(tls_err)
{
int fd, cfd;
--
2.56.0
next prev parent reply other threads:[~2026-11-11 18:55 UTC|newest]
Thread overview: 6+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-11 18:55 [PATCH net v2 0/5] net: tls: break the connection after a burst of empty data records Jakub Kicinski
2026-11-11 18:55 ` [PATCH net v2 1/5] tls: Break " Jakub Kicinski
2026-11-11 18:55 ` [PATCH net v2 2/5] tls: Consume empty data records in tls_sw_splice_read() Jakub Kicinski
2026-11-11 18:55 ` [PATCH net v2 3/5] selftests: tls: Skip the zero_len tests when TLS is unavailable Jakub Kicinski
2026-11-11 18:55 ` Jakub Kicinski [this message]
2026-11-11 18:55 ` [PATCH net v2 5/5] selftests: tls: Cover the cap on consecutive empty records Jakub Kicinski
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=20261111185522.2859004-5-kuba@kernel.org \
--to=kuba@kernel.org \
--cc=andrew+netdev@lunn.ch \
--cc=cel@kernel.org \
--cc=davem@davemloft.net \
--cc=edumazet@google.com \
--cc=horms@kernel.org \
--cc=john.fastabend@gmail.com \
--cc=linux-kselftest@vger.kernel.org \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=sd@queasysnail.net \
--cc=shuah@kernel.org \
/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