* [PATCH net-next v6 1/9] tls: Bound consecutive no-data records in tls_sw_read_sock()
2026-10-19 16:55 [PATCH net-next v6 0/9] net/tls: Receive-path fixes for zero-length data records Chuck Lever
@ 2026-10-19 16:55 ` Chuck Lever
2026-10-22 19:57 ` netdev-bot+sashiko
2026-10-19 16:55 ` [PATCH net-next v6 2/9] tls: Check for a pending signal after an empty record Chuck Lever
` (8 subsequent siblings)
9 siblings, 1 reply; 18+ messages in thread
From: Chuck Lever @ 2026-10-19 16:55 UTC (permalink / raw)
To: John Fastabend, Jakub Kicinski, Sabrina Dubroca, David S. Miller,
Paolo Abeni, Simon Horman, Chuck Lever, Dave Watson, Shuah Khan,
Qingfang Deng, Eric Dumazet
Cc: netdev, linux-kselftest
A zero-length application_data record does not deliver a payload, so
tls_sw_read_sock() never runs the read_actor for one. A peer that
streams such records keeps the loop running, with the socket lock
held, for as long as they arrive. The consumer regains control only
in its read_actor, so it cannot stop a run that never calls the
read_actor.
An LLM audit of the TLS read paths found this defect. The defect has
not been reproduced.
Stop after TLS_RX_NODATA_LIMIT consecutive empty data records. A
record that delivers bytes resets the count, because it runs the
read_actor and the consumer can stop there. Stopping with nothing
copied returns zero, which a read_sock consumer takes as no progress
rather than EOF. A record parsed during the call but not yet
delivered needs the consumer called again. tls_rx_reader_release()
wakes only the callback saved when TLS_RX was set up. nvme-tcp
installs its own callback after the handshake, so that wake does not
reach nvme-tcp. Run the consumer's current sk_data_ready() from a work
item once the reader has returned. A direct call from inside the
reader can re-enter tls_sw_read_sock() through a strparser callback.
The work item runs on tls_strp_wq, which is WQ_MEM_RECLAIM, because
nvme-tcp over TLS can sit on the writeback path.
Fixes: 3be28e2c9cd0 ("net/tls: Consume empty data records in tls_sw_read_sock()")
Signed-off-by: Chuck Lever <cel@kernel.org>
---
include/net/tls.h | 1 +
net/tls/tls.h | 2 ++
net/tls/tls_strp.c | 2 +-
net/tls/tls_sw.c | 30 ++++++++++++++++++++++++++++++
4 files changed, 34 insertions(+), 1 deletion(-)
diff --git a/include/net/tls.h b/include/net/tls.h
index e57bef58851e..42fcb9792bcc 100644
--- a/include/net/tls.h
+++ b/include/net/tls.h
@@ -145,6 +145,7 @@ struct tls_sw_context_rx {
atomic_t decrypt_pending;
struct sk_buff_head async_hold;
struct wait_queue_head wq;
+ struct work_struct data_ready_work;
};
struct tls_record_info {
diff --git a/net/tls/tls.h b/net/tls/tls.h
index 60a37bdaaa25..c9e3d02ac2c2 100644
--- a/net/tls/tls.h
+++ b/net/tls/tls.h
@@ -184,6 +184,8 @@ int tls_sw_fallback_init(struct sock *sk,
struct tls_offload_context_tx *offload_ctx,
struct tls_crypto_info *crypto_info);
+extern struct workqueue_struct *tls_strp_wq;
+
int tls_strp_dev_init(void);
void tls_strp_dev_exit(void);
diff --git a/net/tls/tls_strp.c b/net/tls/tls_strp.c
index 6cc222008d95..ca8cf8ad80a2 100644
--- a/net/tls/tls_strp.c
+++ b/net/tls/tls_strp.c
@@ -11,7 +11,7 @@
#include "tls.h"
-static struct workqueue_struct *tls_strp_wq;
+struct workqueue_struct *tls_strp_wq;
void tls_strp_abort_strp(struct tls_strparser *strp, int err)
{
diff --git a/net/tls/tls_sw.c b/net/tls/tls_sw.c
index 312e51270f29..0ef0edd51edd 100644
--- a/net/tls/tls_sw.c
+++ b/net/tls/tls_sw.c
@@ -2070,6 +2070,22 @@ ssize_t tls_sw_splice_read(struct socket *sock, loff_t *ppos,
goto splice_read_end;
}
+static void tls_rx_data_ready_work(struct work_struct *w)
+{
+ struct tls_sw_context_rx *ctx =
+ container_of(w, struct tls_sw_context_rx, data_ready_work);
+ struct sock *sk = ctx->strp.sk;
+
+ /* strp_data_ready() reads in place from a socket it finds unowned. */
+ lock_sock(sk);
+ /* Past tls_strp_stop(), sk_data_ready is no longer the consumer's. */
+ if (!ctx->strp.stopped)
+ READ_ONCE(sk->sk_data_ready)(sk);
+ release_sock(sk);
+}
+
+#define TLS_RX_NODATA_LIMIT 16
+
int tls_sw_read_sock(struct sock *sk, read_descriptor_t *desc,
sk_read_actor_t read_actor)
{
@@ -2078,6 +2094,7 @@ int tls_sw_read_sock(struct sock *sk, read_descriptor_t *desc,
struct tls_prot_info *prot = &tls_ctx->prot_info;
struct strp_msg *rxm = NULL;
struct sk_buff *skb = NULL;
+ unsigned int nodata = 0;
struct sk_psock *psock;
size_t flushed_at = 0;
bool released = true;
@@ -2144,6 +2161,16 @@ int tls_sw_read_sock(struct sock *sk, read_descriptor_t *desc,
*/
if (rxm->full_len == 0) {
consume_skb(skb);
+ if (++nodata >= TLS_RX_NODATA_LIMIT) {
+ /* tls_rx_reader_release() wakes only the
+ * callback saved when TLS_RX was set up,
+ * not one installed since.
+ */
+ if (tls_strp_msg_ready(ctx))
+ queue_work(tls_strp_wq,
+ &ctx->data_ready_work);
+ break;
+ }
continue;
}
@@ -2154,6 +2181,7 @@ int tls_sw_read_sock(struct sock *sk, read_descriptor_t *desc,
goto read_sock_requeue;
}
copied += used;
+ nodata = 0;
if (used < rxm->full_len) {
rxm->offset += used;
rxm->full_len -= used;
@@ -2345,6 +2373,7 @@ void tls_sw_strparser_done(struct tls_context *tls_ctx)
{
struct tls_sw_context_rx *ctx = tls_sw_ctx_rx(tls_ctx);
+ cancel_work_sync(&ctx->data_ready_work);
tls_strp_done(&ctx->strp);
}
@@ -2476,6 +2505,7 @@ static struct tls_sw_context_rx *init_ctx_rx(struct tls_context *ctx)
crypto_init_wait(&sw_ctx_rx->async_wait);
atomic_set(&sw_ctx_rx->decrypt_pending, 1);
init_waitqueue_head(&sw_ctx_rx->wq);
+ INIT_WORK(&sw_ctx_rx->data_ready_work, tls_rx_data_ready_work);
skb_queue_head_init(&sw_ctx_rx->rx_list);
skb_queue_head_init(&sw_ctx_rx->async_hold);
--
2.55.0
^ permalink raw reply related [flat|nested] 18+ messages in thread* Re: [PATCH net-next v6 1/9] tls: Bound consecutive no-data records in tls_sw_read_sock()
2026-10-19 16:55 ` [PATCH net-next v6 1/9] tls: Bound consecutive no-data records in tls_sw_read_sock() Chuck Lever
@ 2026-10-22 19:57 ` netdev-bot+sashiko
2026-10-23 15:15 ` Chuck Lever
0 siblings, 1 reply; 18+ messages in thread
From: netdev-bot+sashiko @ 2026-10-22 19:57 UTC (permalink / raw)
To: cel
Cc: john.fastabend, kuba, sd, davem, pabeni, horms, davejwatson,
shuah, qingfang.deng, edumazet, netdev, linux-kselftest
Thank you for your contribution! Sashiko AI review found 1 potential
issue(s) to consider.
Critical: 0 · High: 0 · Medium: 1 · Low: 0
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [Medium] When the new TLS_RX_NODATA_LIMIT break fires,
tls_sw_read_sock() queues data_ready_work only if…
--
tls: Bound consecutive no-data records in tls_sw_read_sock()
This patch stops tls_sw_read_sock() after TLS_RX_NODATA_LIMIT consecutive
zero-length data records. If a parsed record is ready when the loop stops,
a work item on tls_strp_wq runs the consumer's current sk_data_ready(),
because tls_rx_reader_release() only wakes saved_data_ready.
> diff --git a/net/tls/tls_sw.c b/net/tls/tls_sw.c
> index 312e51270f29..0ef0edd51edd 100644
> --- a/net/tls/tls_sw.c
> +++ b/net/tls/tls_sw.c
[ ... ]
> @@ -2144,6 +2161,16 @@ int tls_sw_read_sock(struct sock *sk, read_descriptor_t *desc,
> */
> if (rxm->full_len == 0) {
> consume_skb(skb);
> + if (++nodata >= TLS_RX_NODATA_LIMIT) {
> + /* tls_rx_reader_release() wakes only the
> + * callback saved when TLS_RX was set up,
> + * not one installed since.
> + */
> + if (tls_strp_msg_ready(ctx))
> + queue_work(tls_strp_wq,
> + &ctx->data_ready_work);
> + break;
> + }
[Severity: Medium]
Can this break return 0 while the next record is still being parsed, with
nothing left to wake nvme-tcp?
data_ready_work is only queued if msg_ready is already set. For the 16th
empty record, tls_rx_rec_done() calls tls_strp_check_rcv(&ctx->strp, false).
If tls_strp_read_sock() returns -ENOMEM, the parse is handed off to
strp->work and msg_ready stays 0:
net/tls/tls_strp.c:tls_strp_check_rcv() {
...
if (tls_strp_read_sock(strp) == -ENOMEM)
queue_work(tls_strp_wq, &strp->work);
...
}
That -ENOMEM can come from alloc_page(sk->sk_allocation) in
tls_strp_read_copy(), and nvme-tcp sets sk_allocation to GFP_ATOMIC. The
copy path runs in copy_mode, or when tls_strp_check_queue_ok() fails on a
fully received record.
Once the loop breaks, tls_strp_msg_ready(ctx) is false and the work item is
not queued. tls_rx_reader_release() calls tls_rx_msg_maybe_announce(),
which also returns early:
if (!READ_ONCE(strp->msg_ready) || strp->msg_announced)
return;
Later, tls_strp_work() parses the record via tls_strp_check_rcv(strp, true).
It announces the record only through ctx->saved_data_ready, which for
nvme-tcp is the pre-handshake callback and not nvme_tcp_data_ready(). The
commit message makes the same point.
nvme_tcp_io_work() sees a 0 result from read_sock and does not requeue
itself. Does the fully received record then sit until another TCP segment
arrives or the I/O times out?
Before this patch, the loop went back into tls_rx_rec_wait(). With
sk_receive_queue non-empty, that retried tls_strp_check_rcv()
synchronously, so this state needed two allocation failures in a row. With
the early break, a single failure is enough.
A related case, with empty records left on ctx->rx_list by MSG_PEEK, can be
reached at this commit. It goes away later in the series, because "tls:
Consume empty data records in tls_sw_recvmsg()" keeps empty records off
rx_list.
> continue;
> }
>
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261019-tls-follow-on-v6-0-ab01c4ecca0c%40kernel.org
^ permalink raw reply [flat|nested] 18+ messages in thread* Re: [PATCH net-next v6 1/9] tls: Bound consecutive no-data records in tls_sw_read_sock()
2026-10-22 19:57 ` netdev-bot+sashiko
@ 2026-10-23 15:15 ` Chuck Lever
0 siblings, 0 replies; 18+ messages in thread
From: Chuck Lever @ 2026-10-23 15:15 UTC (permalink / raw)
To: netdev-bot+sashiko
Cc: john.fastabend, kuba, sd, davem, pabeni, horms, davejwatson,
shuah, qingfang.deng, edumazet, netdev, linux-kselftest
On Thu, 22 Oct 2026, netdev-bot+sashiko@kernel.org wrote:
> [Severity: Medium]
> Can this break return 0 while the next record is still being parsed, with
> nothing left to wake nvme-tcp?
>
> data_ready_work is only queued if msg_ready is already set. For the 16th
> empty record, tls_rx_rec_done() calls tls_strp_check_rcv(&ctx->strp, false).
> If tls_strp_read_sock() returns -ENOMEM, the parse is handed off to
> strp->work and msg_ready stays 0:
Agreed... but the gap it leads to is not one this patch opens.
When the strparser defers a parse to strp->work, tls_strp_work()
announces the record through saved_data_ready alone, and a
read_sock consumer that installed its own callback after the
handshake never hears about it. That is reachable today after
any record, data or empty.
What the cap changes is the number of allocation failures that
reach that state at one point in the loop, from two to one. Both
need GFP_ATOMIC to fail during a run of sixteen consecutive empty
records, and both end in the same place. The fix is in the deferred
announce, so that a parse completed by strp->work reaches the
callback the consumer installed. My preference would be to fix that
as a separate patch so it can get backported.
If netdev maintainers would rather have the work item queued whenever
the receive queue is non-empty at the break, that is a one-line
change and I can send a v7 with it. Otherwise I plan to leave this
patch as is. Let me know.
--
Chuck Lever (Come to NFS bake-a-thon! https://nfsv4bat.org)
^ permalink raw reply [flat|nested] 18+ messages in thread
* [PATCH net-next v6 2/9] tls: Check for a pending signal after an empty record
2026-10-19 16:55 [PATCH net-next v6 0/9] net/tls: Receive-path fixes for zero-length data records Chuck Lever
2026-10-19 16:55 ` [PATCH net-next v6 1/9] tls: Bound consecutive no-data records in tls_sw_read_sock() Chuck Lever
@ 2026-10-19 16:55 ` Chuck Lever
2026-10-22 19:57 ` netdev-bot+sashiko
2026-10-19 16:55 ` [PATCH net-next v6 3/9] tls: Honor O_NONBLOCK in tls_sw_splice_read() Chuck Lever
` (7 subsequent siblings)
9 siblings, 1 reply; 18+ messages in thread
From: Chuck Lever @ 2026-10-19 16:55 UTC (permalink / raw)
To: John Fastabend, Jakub Kicinski, Sabrina Dubroca, David S. Miller,
Paolo Abeni, Simon Horman, Chuck Lever, Dave Watson, Shuah Khan,
Qingfang Deng, Eric Dumazet
Cc: netdev, linux-kselftest, sashiko-bot
tls_rx_rec_wait() tests signal_pending() only after it sleeps, and
it skips the wait loop when a record is already parsed.
tls_sw_recvmsg() calls tls_rx_rec_wait() once per record, and a
zero-length application_data record does not advance either bound
of the receive loop. A peer that streams such records holds the
caller in recvmsg(), unresponsive to SIGKILL, for as long as they
arrive. The hang has not been reproduced.
Test for a pending signal in tls_sw_recvmsg() before it fetches the
record after an empty one. A reader that has copied data gets the
data. A nonblocking reader with a signal pending gets -EAGAIN
rather than -EINTR. Pending task work alone makes signal_pending()
true, and io_uring retries a nonblocking receive only on -EAGAIN.
The test stays out of tls_rx_rec_wait() because tls_sw_read_sock()
also calls tls_rx_rec_wait(), from callers that cannot act on a
signal.
Decrypt an empty record synchronously. The code at recv_end
overwrites err once any record has been decrypted asynchronously.
With only empty records queued, the reader then gets zero, not the
errno.
Fixes: c46234ebb4d1 ("tls: RX path for ktls")
Reported-by: sashiko-bot <sashiko-bot@kernel.org>
Closes: https://sashiko.dev/#/patchset/20260720-tcp-read-sock-v2-0-29545d034f3c@kernel.org?part=1
Signed-off-by: Chuck Lever <cel@kernel.org>
---
net/tls/tls_sw.c | 27 ++++++++++++++++++++++++---
1 file changed, 24 insertions(+), 3 deletions(-)
diff --git a/net/tls/tls_sw.c b/net/tls/tls_sw.c
index 0ef0edd51edd..e8a9f0c91e81 100644
--- a/net/tls/tls_sw.c
+++ b/net/tls/tls_sw.c
@@ -1734,6 +1734,14 @@ tls_read_flush_backlog(struct sock *sk, struct tls_prot_info *prot,
return sk_flush_backlog(sk);
}
+/* io_uring retries a nonblocking receive only on -EAGAIN. */
+static int tls_rx_signal_err(struct sock *sk, bool nonblock)
+{
+ long timeo = sock_rcvtimeo(sk, nonblock);
+
+ return timeo ? sock_intr_errno(timeo) : -EAGAIN;
+}
+
static int tls_rx_reader_acquire(struct sock *sk, struct tls_sw_context_rx *ctx,
bool nonblock)
{
@@ -1755,7 +1763,7 @@ static int tls_rx_reader_acquire(struct sock *sk, struct tls_sw_context_rx *ctx,
if (timeo <= 0)
return -EAGAIN;
if (signal_pending(current))
- return sock_intr_errno(timeo);
+ return tls_rx_signal_err(sk, nonblock);
if (ret < 0)
return ret;
}
@@ -1824,6 +1832,7 @@ int tls_sw_recvmsg(struct sock *sk,
bool is_peek = flags & MSG_PEEK;
bool rx_more = false;
bool released = true;
+ bool nodata = false;
bool zc_capable;
if (unlikely(flags & MSG_ERRQUEUE))
@@ -1858,6 +1867,14 @@ int tls_sw_recvmsg(struct sock *sk,
struct tls_decrypt_arg darg;
int to_decrypt, chunk;
+ /* A run of empty records advances neither loop bound, and
+ * tls_rx_rec_wait() tests for a signal only after sleeping.
+ */
+ if (nodata && signal_pending(current)) {
+ err = tls_rx_signal_err(sk, flags & MSG_DONTWAIT);
+ goto recv_end;
+ }
+
err = tls_rx_rec_wait(sk, flags & MSG_DONTWAIT,
released, !!(decrypted + copied));
if (err <= 0)
@@ -1874,9 +1891,12 @@ int tls_sw_recvmsg(struct sock *sk,
tlm->control == TLS_RECORD_TYPE_DATA)
darg.zc = true;
- /* Do not use async mode if record is non-data */
+ /* Do not use async mode if record is non-data or empty.
+ * Once a record has gone async, recv_end discards the
+ * error that ends a run of empty records.
+ */
if (tlm->control == TLS_RECORD_TYPE_DATA)
- darg.async = ctx->async_capable;
+ darg.async = ctx->async_capable && to_decrypt;
else
darg.async = false;
@@ -1910,6 +1930,7 @@ int tls_sw_recvmsg(struct sock *sk,
/* TLS 1.3 may have updated the length by more than overhead */
rxm = strp_msg(darg.skb);
chunk = rxm->full_len;
+ nodata = !chunk;
tls_rx_rec_done(ctx);
if (!darg.zc) {
--
2.55.0
^ permalink raw reply related [flat|nested] 18+ messages in thread* Re: [PATCH net-next v6 2/9] tls: Check for a pending signal after an empty record
2026-10-19 16:55 ` [PATCH net-next v6 2/9] tls: Check for a pending signal after an empty record Chuck Lever
@ 2026-10-22 19:57 ` netdev-bot+sashiko
2026-10-23 15:18 ` Chuck Lever
0 siblings, 1 reply; 18+ messages in thread
From: netdev-bot+sashiko @ 2026-10-22 19:57 UTC (permalink / raw)
To: cel
Cc: john.fastabend, kuba, sd, davem, pabeni, horms, davejwatson,
shuah, qingfang.deng, edumazet, netdev, linux-kselftest
Thank you for your contribution! Sashiko AI review found 1 potential
issue(s) to consider.
Critical: 0 · High: 1 (1) · Medium: 0 · Low: 0
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [High] The only new exit from a run of empty records in
tls_sw_recvmsg() is `if (nodata && signal_pending(current))`.
Pre-existing issues:
- [High] At this commit, a blocking recvmsg(MSG_PEEK) queues every zero-
length application_data record on ctx->rx_list as a cleartext skb…
--
tls: Check for a pending signal after an empty record
This commit adds a signal_pending() test in tls_sw_recvmsg() before the
next record is fetched after a zero-length application_data record. The
goal is that a peer streaming empty records cannot hold the reader in
recvmsg() indefinitely. Nonblocking readers with a signal pending get
-EAGAIN, and empty records are always decrypted synchronously.
Closes: https://sashiko.dev/#/patchset/20260720-tcp-read-sock-v2-0-29545d034f3c@kernel.org?part=1
> diff --git a/net/tls/tls_sw.c b/net/tls/tls_sw.c
> index 0ef0edd51edd..e8a9f0c91e81 100644
> --- a/net/tls/tls_sw.c
> +++ b/net/tls/tls_sw.c
[ ... ]
> @@ -1858,6 +1867,14 @@ int tls_sw_recvmsg(struct sock *sk,
> struct tls_decrypt_arg darg;
> int to_decrypt, chunk;
>
> + /* A run of empty records advances neither loop bound, and
> + * tls_rx_rec_wait() tests for a signal only after sleeping.
> + */
> + if (nodata && signal_pending(current)) {
> + err = tls_rx_signal_err(sk, flags & MSG_DONTWAIT);
> + goto recv_end;
> + }
[Severity: High]
Does this close the hang for callers that never receive a signal?
An empty record changes neither len nor decrypted, so the loop condition
stays true:
while (len && (decrypted + copied < target || tls_strp_msg_ready(ctx))) {
When another record is already parsed, tls_rx_rec_wait() skips its wait
loop entirely. Neither its nonblocking exit nor SO_RCVTIMEO is reached:
while (!tls_strp_msg_ready(ctx)) {
...
if (!timeo)
return -EAGAIN;
...
}
That leaves signal_pending() as the only way out of a run of empty
records.
Two kinds of caller appear to still be held for as long as the records
keep arriving:
- nonblocking readers (MSG_DONTWAIT or O_NONBLOCK) with no signal
pending
- in-kernel callers that do not take signals. One is nfsd threads, via
svc_tcp_sock_recvmsg()->sock_recvmsg(sock, msg, MSG_DONTWAIT) on
kTLS sockets (there is no allow_signal() in fs/nfsd/). Another is
rpciod workers, via xs_read_stream(transport, MSG_DONTWAIT).
In the nfsd case the thread spins in tls_sw_recvmsg() with the socket
lock held, so kthread_stop() is blocked as well.
The commit message says tls_sw_read_sock() has "callers that cannot act
on a signal". The earlier commit "tls: Bound consecutive no-data records
in tls_sw_read_sock()" bounds that path with TLS_RX_NODATA_LIMIT.
tls_sw_recvmsg() has callers of the same kind, but it gets no count bound
and no nonblocking exit.
The later commit "tls: Return copied data ahead of a run of empty records"
adds a break when decrypted + copied >= target. That covers a reader that
already has data. The case where nothing was copied remains, and the
series says so: "A reader that has copied nothing still reads empty
records for as long as they arrive".
Could the run also be ended for nonblocking readers? One option is to
return -EAGAIN after an empty record. Another is to stop after a bounded
count, as tls_sw_read_sock() does.
> +
> err = tls_rx_rec_wait(sk, flags & MSG_DONTWAIT,
> released, !!(decrypted + copied));
> if (err <= 0)
[ ... ]
> @@ -1910,6 +1930,7 @@ int tls_sw_recvmsg(struct sock *sk,
> /* TLS 1.3 may have updated the length by more than overhead */
> rxm = strp_msg(darg.skb);
> chunk = rxm->full_len;
> + nodata = !chunk;
> tls_rx_rec_done(ctx);
>
> if (!darg.zc) {
[Severity: High]
This is a pre-existing issue and was not introduced by this patch. Can a
blocking recvmsg(MSG_PEEK) grow ctx->rx_list without bound here?
MSG_PEEK disables zero-copy, so every empty application_data record takes
the peek arm further down in tls_sw_recvmsg():
if (is_peek) {
peeked += chunk;
goto put_on_rx_list;
}
...
put_on_rx_list:
decrypted += chunk;
len -= chunk;
__skb_queue_tail(&ctx->rx_list, skb);
With chunk == 0, neither decrypted nor len advances. The loop keeps
accepting records and queues a cleartext skb for each one.
tcp_read_done() has already released the TCP-side data, so TCP flow
control does not push back on the peer.
The later commit "tls: Consume empty data records in tls_sw_recvmsg()"
fixes this. Its message says "The peek arm and the async arm queue each
empty record on rx_list, and a peer that streams empty records grows
rx_list without bound". At the end of the series, the empty-record branch
consumes the skb before the peek arm is reached.
The async arm also cannot queue empty records at that point. TLS 1.2
empty records are decrypted synchronously, and async is disabled for
TLS 1.3.
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261019-tls-follow-on-v6-0-ab01c4ecca0c%40kernel.org
^ permalink raw reply [flat|nested] 18+ messages in thread* Re: [PATCH net-next v6 2/9] tls: Check for a pending signal after an empty record
2026-10-22 19:57 ` netdev-bot+sashiko
@ 2026-10-23 15:18 ` Chuck Lever
0 siblings, 0 replies; 18+ messages in thread
From: Chuck Lever @ 2026-10-23 15:18 UTC (permalink / raw)
To: netdev-bot+sashiko
Cc: john.fastabend, kuba, sd, davem, pabeni, horms, davejwatson,
shuah, qingfang.deng, edumazet, netdev, linux-kselftest
On Thu, 22 Oct 2026, netdev-bot+sashiko@kernel.org wrote:
> [Severity: High]
> Does this close the hang for callers that never receive a signal?
The cover letter scopes the count bound to tls_sw_read_sock() and
leaves recvmsg() with the signal test on purpose. I consider
addressing the hang as out of scope.
> Two kinds of caller appear to still be held for as long as the records
> keep arriving:
>
> - nonblocking readers (MSG_DONTWAIT or O_NONBLOCK) with no signal
> pending
>
> - in-kernel callers that do not take signals. One is nfsd threads, via
> svc_tcp_sock_recvmsg()->sock_recvmsg(sock, msg, MSG_DONTWAIT) on
> kTLS sockets (there is no allow_signal() in fs/nfsd/). Another is
> rpciod workers, via xs_read_stream(transport, MSG_DONTWAIT).
Both exit through tls_rx_rec_wait() with -EAGAIN as soon as the
receive queue drains. The hold lasts while the peer delivers
empty records faster than the reader decrypts them, and that is
the behavior these callers have today. This patch narrows it for
the callers that can take a signal and leaves the rest where they
are.
> Could the run also be ended for nonblocking readers? One option is to
> return -EAGAIN after an empty record. Another is to stop after a bounded
> count, as tls_sw_read_sock() does.
An -EAGAIN after every empty record returns a readable socket to
the caller with nothing copied and the next record already queued,
so a poll-driven reader spins on it. A count bound fits read_sock
because a zero return there means no progress, and a work item can
call the consumer back. recvmsg() has no equivalent return for a
blocking reader that has copied nothing. The in-kernel callers hold
the socket lock across the run and cannot take a signal, as the
cover letter states. If a bound for them turns out to be needed,
it can be made as a separate change with its own Fixes: tag.
I'd like to leave this patch as it is.
--
Chuck Lever (Come to NFS bake-a-thon! https://nfsv4bat.org)
^ permalink raw reply [flat|nested] 18+ messages in thread
* [PATCH net-next v6 3/9] tls: Honor O_NONBLOCK in tls_sw_splice_read()
2026-10-19 16:55 [PATCH net-next v6 0/9] net/tls: Receive-path fixes for zero-length data records Chuck Lever
2026-10-19 16:55 ` [PATCH net-next v6 1/9] tls: Bound consecutive no-data records in tls_sw_read_sock() Chuck Lever
2026-10-19 16:55 ` [PATCH net-next v6 2/9] tls: Check for a pending signal after an empty record Chuck Lever
@ 2026-10-19 16:55 ` Chuck Lever
2026-10-19 16:55 ` [PATCH net-next v6 4/9] tls: Consume empty data records " Chuck Lever
` (6 subsequent siblings)
9 siblings, 0 replies; 18+ messages in thread
From: Chuck Lever @ 2026-10-19 16:55 UTC (permalink / raw)
To: John Fastabend, Jakub Kicinski, Sabrina Dubroca, David S. Miller,
Paolo Abeni, Simon Horman, Chuck Lever, Dave Watson, Shuah Khan,
Qingfang Deng, Eric Dumazet
Cc: netdev, linux-kselftest
tls_sw_splice_read() takes its blocking behavior from
SPLICE_F_NONBLOCK alone. When splicing from a socket to a pipe,
do_splice() sets that flag from the pipe's O_NONBLOCK, not the
socket's. On a nonblocking socket, a splice(2) call without
SPLICE_F_NONBLOCK therefore sleeps in tls_rx_rec_wait() until a
record arrives. tcp_splice_read() instead reads sock->file->f_flags
and returns -EAGAIN.
Polling first does not avoid the sleep once tls_sw_splice_read()
consumes zero-length records, as it does after the next patch in
this series. tls_sw_sock_is_readable() reports the socket readable
while any record sits on rx_list, including a zero-length record
that delivers no bytes to the pipe. The strparser announces a record
before decryption, when the record's plaintext length is unknown, so
the readiness test cannot screen such a record out. A splice that
consumes the record and waits for the next stalls every connection
an event loop multiplexes.
An LLM audit of the TLS read paths found this defect. With empty
records consumed, the zero_len_splice selftest that a later patch in
this series adds, run on a nonblocking socket, reproduces the sleep.
Treat the socket's O_NONBLOCK as nonblocking too. A caller that
sets O_NONBLOCK on the socket and splices without SPLICE_F_NONBLOCK
now gets -EAGAIN where the splice blocked before. sendfile(2) from
a TLS socket changes the same way, because do_sendfile() leaves the
input file's O_NONBLOCK out of the splice flags. Both now behave as
they do on a plain TCP socket.
Fixes: c46234ebb4d1 ("tls: RX path for ktls")
Signed-off-by: Chuck Lever <cel@kernel.org>
---
net/tls/tls_sw.c | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/net/tls/tls_sw.c b/net/tls/tls_sw.c
index e8a9f0c91e81..d63b4c71780a 100644
--- a/net/tls/tls_sw.c
+++ b/net/tls/tls_sw.c
@@ -2028,10 +2028,14 @@ ssize_t tls_sw_splice_read(struct socket *sock, loff_t *ppos,
struct tls_msg *tlm;
struct sk_buff *skb;
ssize_t copied = 0;
+ bool nonblock;
int chunk;
int err;
- err = tls_rx_reader_lock(sk, ctx, flags & SPLICE_F_NONBLOCK);
+ nonblock = (flags & SPLICE_F_NONBLOCK) ||
+ (sock->file->f_flags & O_NONBLOCK);
+
+ err = tls_rx_reader_lock(sk, ctx, nonblock);
if (err < 0)
return err;
@@ -2045,8 +2049,7 @@ ssize_t tls_sw_splice_read(struct socket *sock, loff_t *ppos,
} else {
struct tls_decrypt_arg darg;
- err = tls_rx_rec_wait(sk, flags & SPLICE_F_NONBLOCK,
- true, false);
+ err = tls_rx_rec_wait(sk, nonblock, true, false);
if (err <= 0)
goto splice_read_end;
--
2.55.0
^ permalink raw reply related [flat|nested] 18+ messages in thread* [PATCH net-next v6 4/9] tls: Consume empty data records in tls_sw_splice_read()
2026-10-19 16:55 [PATCH net-next v6 0/9] net/tls: Receive-path fixes for zero-length data records Chuck Lever
` (2 preceding siblings ...)
2026-10-19 16:55 ` [PATCH net-next v6 3/9] tls: Honor O_NONBLOCK in tls_sw_splice_read() Chuck Lever
@ 2026-10-19 16:55 ` Chuck Lever
2026-10-19 16:55 ` [PATCH net-next v6 5/9] tls: Consume empty data records in tls_sw_recvmsg() Chuck Lever
` (5 subsequent siblings)
9 siblings, 0 replies; 18+ messages in thread
From: Chuck Lever @ 2026-10-19 16:55 UTC (permalink / raw)
To: John Fastabend, Jakub Kicinski, Sabrina Dubroca, David S. Miller,
Paolo Abeni, Simon Horman, Chuck Lever, Dave Watson, Shuah Khan,
Qingfang Deng, Eric Dumazet
Cc: netdev, linux-kselftest
A zero-length application_data record decrypts to full_len == 0,
so tls_sw_splice_read() splices zero bytes and returns zero. A zero
return from a splice read means EOF, and the caller tears down a
connection that is still live.
Consume the record and fetch the next one, as tls_sw_recvmsg()
does. Test for a pending signal before each retry, so the splicing
task stays killable. A nonblocking splice gets -EAGAIN, as
tls_sw_recvmsg() does. After this change, tls_sw_splice_read() reads
empty records for as long as they arrive, until a signal is pending.
When an empty record precedes a close_notify, splice(2) now returns
-EINVAL from the control-record test instead of a false EOF. A
splice that reaches an alert record already reports -EINVAL.
Fixes: c46234ebb4d1 ("tls: RX path for ktls")
Reported-by: Sabrina Dubroca <sd@queasysnail.net>
Closes: https://lore.kernel.org/netdev/akaoXcfamBp8_mYe@krikkit/
Signed-off-by: Chuck Lever <cel@kernel.org>
---
net/tls/tls_sw.c | 17 ++++++++++++++++-
1 file changed, 16 insertions(+), 1 deletion(-)
diff --git a/net/tls/tls_sw.c b/net/tls/tls_sw.c
index d63b4c71780a..fe9c5c21dadc 100644
--- a/net/tls/tls_sw.c
+++ b/net/tls/tls_sw.c
@@ -2027,6 +2027,7 @@ ssize_t tls_sw_splice_read(struct socket *sock, loff_t *ppos,
struct sock *sk = sock->sk;
struct tls_msg *tlm;
struct sk_buff *skb;
+ bool released = true;
ssize_t copied = 0;
bool nonblock;
int chunk;
@@ -2044,12 +2045,13 @@ ssize_t tls_sw_splice_read(struct socket *sock, loff_t *ppos,
if (err)
goto splice_read_end;
+retry:
if (!skb_queue_empty(&ctx->rx_list)) {
skb = __skb_dequeue(&ctx->rx_list);
} else {
struct tls_decrypt_arg darg;
- err = tls_rx_rec_wait(sk, nonblock, true, false);
+ err = tls_rx_rec_wait(sk, nonblock, released, false);
if (err <= 0)
goto splice_read_end;
@@ -2061,6 +2063,9 @@ ssize_t tls_sw_splice_read(struct socket *sock, loff_t *ppos,
tls_rx_rec_done(ctx);
skb = darg.skb;
+
+ /* The retry's wait runs with the socket lock still held. */
+ released = false;
}
rxm = strp_msg(skb);
@@ -2072,6 +2077,16 @@ ssize_t tls_sw_splice_read(struct socket *sock, loff_t *ppos,
goto splice_requeue;
}
+ /* Splicing zero bytes reads as EOF to the caller. */
+ if (rxm->full_len == 0) {
+ consume_skb(skb);
+ if (signal_pending(current)) {
+ err = tls_rx_signal_err(sk, nonblock);
+ goto splice_read_end;
+ }
+ goto retry;
+ }
+
chunk = min_t(unsigned int, rxm->full_len, len);
copied = skb_splice_bits(skb, sk, rxm->offset, pipe, chunk, flags);
if (copied < 0)
--
2.55.0
^ permalink raw reply related [flat|nested] 18+ messages in thread* [PATCH net-next v6 5/9] tls: Consume empty data records in tls_sw_recvmsg()
2026-10-19 16:55 [PATCH net-next v6 0/9] net/tls: Receive-path fixes for zero-length data records Chuck Lever
` (3 preceding siblings ...)
2026-10-19 16:55 ` [PATCH net-next v6 4/9] tls: Consume empty data records " Chuck Lever
@ 2026-10-19 16:55 ` Chuck Lever
2026-10-19 16:55 ` [PATCH net-next v6 6/9] tls: Return copied data ahead of a run of empty records Chuck Lever
` (4 subsequent siblings)
9 siblings, 0 replies; 18+ messages in thread
From: Chuck Lever @ 2026-10-19 16:55 UTC (permalink / raw)
To: John Fastabend, Jakub Kicinski, Sabrina Dubroca, David S. Miller,
Paolo Abeni, Simon Horman, Chuck Lever, Dave Watson, Shuah Khan,
Qingfang Deng, Eric Dumazet
Cc: netdev, linux-kselftest, sashiko-bot
TLS 1.2 and TLS 1.3 both permit zero-length application_data
records as a traffic-analysis countermeasure (RFC 5246 Section
6.2.1 and RFC 8446 Section 5.1). A record that decrypts to
full_len == 0 advances neither len nor decrypted, so
tls_sw_recvmsg() keeps reading empty records for as long as they
arrive. The peek arm and the async arm queue each empty record on
rx_list, and a peer that streams empty records grows rx_list
without bound. The growth has not been reproduced.
Consume an empty data record as soon as the receive loop has it,
before the paths diverge on darg.zc. Freeing the skb requires its
decryption to have completed, and an empty record is already
decrypted synchronously. The new branch sets MSG_EOR itself,
because the record no longer reaches the assignment at the bottom
of the loop. The peek arm does not set the flag, so after the
change a MSG_PEEK receive that reaches an empty record reports
MSG_EOR where it did not before. Test for a pending signal in the
branch as well, and drop the flag that deferred the test to the
next iteration.
After this change, tls_sw_recvmsg() still reads empty records for
as long as they arrive, until a signal is pending.
Fixes: 692d7b5d1f91 ("tls: Fix recvmsg() to be able to peek across multiple records")
Reported-by: sashiko-bot <sashiko-bot@kernel.org>
Closes: https://sashiko.dev/#/patchset/20260630191551.875664-1-cel@kernel.org?part=1
Signed-off-by: Chuck Lever <cel@kernel.org>
---
net/tls/tls_sw.c | 35 +++++++++++++++++++++++------------
1 file changed, 23 insertions(+), 12 deletions(-)
diff --git a/net/tls/tls_sw.c b/net/tls/tls_sw.c
index fe9c5c21dadc..550308410ff1 100644
--- a/net/tls/tls_sw.c
+++ b/net/tls/tls_sw.c
@@ -1832,7 +1832,6 @@ int tls_sw_recvmsg(struct sock *sk,
bool is_peek = flags & MSG_PEEK;
bool rx_more = false;
bool released = true;
- bool nodata = false;
bool zc_capable;
if (unlikely(flags & MSG_ERRQUEUE))
@@ -1867,14 +1866,6 @@ int tls_sw_recvmsg(struct sock *sk,
struct tls_decrypt_arg darg;
int to_decrypt, chunk;
- /* A run of empty records advances neither loop bound, and
- * tls_rx_rec_wait() tests for a signal only after sleeping.
- */
- if (nodata && signal_pending(current)) {
- err = tls_rx_signal_err(sk, flags & MSG_DONTWAIT);
- goto recv_end;
- }
-
err = tls_rx_rec_wait(sk, flags & MSG_DONTWAIT,
released, !!(decrypted + copied));
if (err <= 0)
@@ -1892,8 +1883,8 @@ int tls_sw_recvmsg(struct sock *sk,
darg.zc = true;
/* Do not use async mode if record is non-data or empty.
- * Once a record has gone async, recv_end discards the
- * error that ends a run of empty records.
+ * The receive loop frees an empty record's skb, which an
+ * async decrypt would still be using.
*/
if (tlm->control == TLS_RECORD_TYPE_DATA)
darg.async = ctx->async_capable && to_decrypt;
@@ -1930,9 +1921,29 @@ int tls_sw_recvmsg(struct sock *sk,
/* TLS 1.3 may have updated the length by more than overhead */
rxm = strp_msg(darg.skb);
chunk = rxm->full_len;
- nodata = !chunk;
tls_rx_rec_done(ctx);
+ /* Keep an empty record off rx_list. On the zero-copy path
+ * darg.skb is the strparser's, released by tls_rx_rec_done().
+ */
+ if (!chunk && control == TLS_RECORD_TYPE_DATA) {
+ if (!darg.zc)
+ consume_skb(darg.skb);
+
+ /* An empty record still marks a boundary. */
+ msg->msg_flags |= MSG_EOR;
+
+ /* A run of empty records advances neither loop bound,
+ * and tls_rx_rec_wait() tests for a signal only after
+ * sleeping.
+ */
+ if (signal_pending(current)) {
+ err = tls_rx_signal_err(sk, flags & MSG_DONTWAIT);
+ goto recv_end;
+ }
+ continue;
+ }
+
if (!darg.zc) {
bool partially_consumed = chunk > len;
struct sk_buff *skb = darg.skb;
--
2.55.0
^ permalink raw reply related [flat|nested] 18+ messages in thread* [PATCH net-next v6 6/9] tls: Return copied data ahead of a run of empty records
2026-10-19 16:55 [PATCH net-next v6 0/9] net/tls: Receive-path fixes for zero-length data records Chuck Lever
` (4 preceding siblings ...)
2026-10-19 16:55 ` [PATCH net-next v6 5/9] tls: Consume empty data records in tls_sw_recvmsg() Chuck Lever
@ 2026-10-19 16:55 ` Chuck Lever
2026-10-19 16:55 ` [PATCH net-next v6 7/9] selftests: tls: Skip the zero_len tests when TLS is unavailable Chuck Lever
` (3 subsequent siblings)
9 siblings, 0 replies; 18+ messages in thread
From: Chuck Lever @ 2026-10-19 16:55 UTC (permalink / raw)
To: John Fastabend, Jakub Kicinski, Sabrina Dubroca, David S. Miller,
Paolo Abeni, Simon Horman, Chuck Lever, Dave Watson, Shuah Khan,
Qingfang Deng, Eric Dumazet
Cc: netdev, linux-kselftest, sashiko-bot
Once tls_sw_recvmsg() has copied enough data to meet the receive
low-water mark, the receive loop continues only while another
record is ready. A zero-length application_data record is consumed
without advancing len, so a peer that follows data with a stream of
empty records keeps the reader in the loop. The reader does not get
the data already copied until the stream stops or a signal is
pending. The delay has not been reproduced.
Leave the receive loop at an empty record once the low-water mark
is met. A reader that has copied nothing still reads empty records
for as long as they arrive.
Fixes: 04b25a5411f9 ("net/tls: fix no wakeup on partial reads")
Suggested-by: sashiko-bot <netdev-bot+sashiko@kernel.org>
Link: https://lore.kernel.org/r/179124254665.434549.12245680127262431659@kernel.org
Signed-off-by: Chuck Lever <cel@kernel.org>
---
net/tls/tls_sw.c | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/net/tls/tls_sw.c b/net/tls/tls_sw.c
index 550308410ff1..599bf1f21189 100644
--- a/net/tls/tls_sw.c
+++ b/net/tls/tls_sw.c
@@ -1933,6 +1933,12 @@ int tls_sw_recvmsg(struct sock *sk,
/* An empty record still marks a boundary. */
msg->msg_flags |= MSG_EOR;
+ /* Return data already copied instead of holding it
+ * through a run of empty records.
+ */
+ if (decrypted + copied >= target)
+ break;
+
/* A run of empty records advances neither loop bound,
* and tls_rx_rec_wait() tests for a signal only after
* sleeping.
--
2.55.0
^ permalink raw reply related [flat|nested] 18+ messages in thread* [PATCH net-next v6 7/9] selftests: tls: Skip the zero_len tests when TLS is unavailable
2026-10-19 16:55 [PATCH net-next v6 0/9] net/tls: Receive-path fixes for zero-length data records Chuck Lever
` (5 preceding siblings ...)
2026-10-19 16:55 ` [PATCH net-next v6 6/9] tls: Return copied data ahead of a run of empty records Chuck Lever
@ 2026-10-19 16:55 ` Chuck Lever
2026-10-19 16:55 ` [PATCH net-next v6 8/9] selftests: tls: Extend coverage for zero-length records Chuck Lever
` (2 subsequent siblings)
9 siblings, 0 replies; 18+ messages in thread
From: Chuck Lever @ 2026-10-19 16:55 UTC (permalink / raw)
To: John Fastabend, Jakub Kicinski, Sabrina Dubroca, David S. Miller,
Paolo Abeni, Simon Horman, Chuck Lever, Dave Watson, Shuah Khan,
Qingfang Deng, Eric Dumazet
Cc: netdev, linux-kselftest
FIXTURE_SETUP(zero_len) returns early when ulp_sock_pair() reports
that the TCP_ULP setsockopt failed, so on a kernel built without
CONFIG_TLS the TLS_RX setsockopt never runs. TEST_F(zero_len, test)
then sends its raw records over a plain TCP socket, which returns
the ciphertext verbatim. All eight variants FAIL on a machine that
lacks TLS, and those failures mask any real regression in the same
run.
Skip the test when the fixture recorded notls, as the other
fixtures in this file do.
Fixes: a61a3e961baf ("selftests: tls: add tests for zero-length records")
Signed-off-by: Chuck Lever <cel@kernel.org>
---
tools/testing/selftests/net/tls.c | 3 +++
1 file changed, 3 insertions(+)
diff --git a/tools/testing/selftests/net/tls.c b/tools/testing/selftests/net/tls.c
index 9d3cd4fff062..0bc43728262e 100644
--- a/tools/testing/selftests/net/tls.c
+++ b/tools/testing/selftests/net/tls.c
@@ -2639,6 +2639,9 @@ TEST_F(zero_len, test)
int rec_off;
int i;
+ 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),
--
2.55.0
^ permalink raw reply related [flat|nested] 18+ messages in thread* [PATCH net-next v6 8/9] selftests: tls: Extend coverage for zero-length records
2026-10-19 16:55 [PATCH net-next v6 0/9] net/tls: Receive-path fixes for zero-length data records Chuck Lever
` (6 preceding siblings ...)
2026-10-19 16:55 ` [PATCH net-next v6 7/9] selftests: tls: Skip the zero_len tests when TLS is unavailable Chuck Lever
@ 2026-10-19 16:55 ` Chuck Lever
2026-10-22 19:57 ` netdev-bot+sashiko
2026-10-19 16:55 ` [PATCH net-next v6 9/9] selftests: tls: Cover splice on a nonblocking socket Chuck Lever
2026-10-26 22:37 ` [PATCH net-next v6 0/9] net/tls: Receive-path fixes for zero-length data records Jakub Kicinski
9 siblings, 1 reply; 18+ messages in thread
From: Chuck Lever @ 2026-10-19 16:55 UTC (permalink / raw)
To: John Fastabend, Jakub Kicinski, Sabrina Dubroca, David S. Miller,
Paolo Abeni, Simon Horman, Chuck Lever, Dave Watson, Shuah Khan,
Qingfang Deng, Eric Dumazet
Cc: netdev, linux-kselftest
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 a plain recv(), so neither splice nor MSG_PEEK is
tested against an empty record. No variant places an empty record
between two data records, the one sequence where returning copied
data ahead of a run of empty records changes what recv() reports.
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.
Add data_0data_data to zero_len. The first recv() returns the first
record alone and leaves the second for the next call. A kernel that
reads through the empty record returns both payloads from the first
call and fails the variant.
Signed-off-by: Chuck Lever <cel@kernel.org>
---
tools/testing/selftests/net/tls.c | 232 +++++++++++++++++++++++++++++++++++---
1 file changed, 215 insertions(+), 17 deletions(-)
diff --git a/tools/testing/selftests/net/tls.c b/tools/testing/selftests/net/tls.c
index 0bc43728262e..aa66d0bbd1b5 100644
--- a/tools/testing/selftests/net/tls.c
+++ b/tools/testing/selftests/net/tls.c
@@ -2549,6 +2549,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;
@@ -2609,21 +2645,15 @@ FIXTURE_VARIANT_ADD(zero_len, data_0data_0data)
.recv_ret = { 11, -EAGAIN, },
};
+FIXTURE_VARIANT_ADD(zero_len, data_0data_data)
+{
+ .recs = { &id0_data_l11, &id1_data_l0, &id2_data_l11, },
+ .recv_ret = { 11, 11, -EAGAIN, },
+};
+
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)
@@ -2642,10 +2672,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;
@@ -2671,6 +2698,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.55.0
^ permalink raw reply related [flat|nested] 18+ messages in thread* Re: [PATCH net-next v6 8/9] selftests: tls: Extend coverage for zero-length records
2026-10-19 16:55 ` [PATCH net-next v6 8/9] selftests: tls: Extend coverage for zero-length records Chuck Lever
@ 2026-10-22 19:57 ` netdev-bot+sashiko
2026-10-23 15:20 ` Chuck Lever
0 siblings, 1 reply; 18+ messages in thread
From: netdev-bot+sashiko @ 2026-10-22 19:57 UTC (permalink / raw)
To: cel
Cc: john.fastabend, kuba, sd, davem, pabeni, horms, davejwatson,
shuah, qingfang.deng, edumazet, netdev, linux-kselftest
Thank you for your contribution! Sashiko AI review found more severe
issue(s) in other patches of this series. This mail is only an FYI, in case
you want to address the nit picks below when you respin.
Low priority / nit picks for your consideration:
- [Low] This patch adds TEST_F(zero_len_splice, test) in
tools/testing/selftests/net/tls.c.
--
selftests: tls: Extend coverage for zero-length records
This adds the zero_len_splice and zero_len_peek fixtures to the TLS
selftests. They check that splice() and MSG_PEEK get past runs of
zero-length application_data records. It also adds a data_0data_data
variant to zero_len, which checks that recv() does not read through an
empty record sitting between two data records.
> diff --git a/tools/testing/selftests/net/tls.c b/tools/testing/selftests/net/tls.c
> index 0bc43728262e..aa66d0bbd1b5 100644
> --- a/tools/testing/selftests/net/tls.c
> +++ b/tools/testing/selftests/net/tls.c
[ ... ]
> +/* 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);
[Severity: Low]
Both splice() calls in TEST_F(zero_len_splice, test) pass
SPLICE_F_NONBLOCK, and nothing sets O_NONBLOCK on cfd. Is the socket
O_NONBLOCK path in tls_sw_splice_read() ever covered by this test?
net/tls/tls_sw.c:tls_sw_splice_read() {
...
nonblock = (flags & SPLICE_F_NONBLOCK) ||
(sock->file->f_flags & O_NONBLOCK);
...
}
The earlier commit "tls: Honor O_NONBLOCK in tls_sw_splice_read()"
added the f_flags check above. It names this selftest, run on a
nonblocking socket, as its reproducer.
As of this patch, SPLICE_F_NONBLOCK on its own makes the wait return
-EAGAIN. A kernel that lost the O_NONBLOCK term would still pass every
zero_len_splice variant.
This looks resolved later in the series. "selftests: tls: Cover splice
on a nonblocking socket" moves the body into zero_len_do_splice() with
a splice_flags parameter. It also adds TEST_F(zero_len_splice,
nonblock_socket), which sets O_NONBLOCK on cfd with fcntl(F_SETFL) and
then splices with flags 0.
Would it make sense to fold that coverage into this patch, so the
reproducer named by the fix is in place from the start?
> + 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);
> + }
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261019-tls-follow-on-v6-0-ab01c4ecca0c%40kernel.org
^ permalink raw reply [flat|nested] 18+ messages in thread* Re: [PATCH net-next v6 8/9] selftests: tls: Extend coverage for zero-length records
2026-10-22 19:57 ` netdev-bot+sashiko
@ 2026-10-23 15:20 ` Chuck Lever
0 siblings, 0 replies; 18+ messages in thread
From: Chuck Lever @ 2026-10-23 15:20 UTC (permalink / raw)
To: netdev-bot+sashiko
Cc: john.fastabend, kuba, sd, davem, pabeni, horms, davejwatson,
shuah, qingfang.deng, edumazet, netdev, linux-kselftest
On Thu, 22 Oct 2026, netdev-bot+sashiko@kernel.org wrote:
> Would it make sense to fold that coverage into this patch, so the
> reproducer named by the fix is in place from the start?
The fixture and the nonblocking-socket variant are separate patches
so that the variant, which pins the behavior change in patch 3, can
be reviewed on its own. The fix and its reproducer are both in the
series, the fix preceding the test. I'd like to keep the series
organized as it is.
--
Chuck Lever (Come to NFS bake-a-thon! https://nfsv4bat.org)
^ permalink raw reply [flat|nested] 18+ messages in thread
* [PATCH net-next v6 9/9] selftests: tls: Cover splice on a nonblocking socket
2026-10-19 16:55 [PATCH net-next v6 0/9] net/tls: Receive-path fixes for zero-length data records Chuck Lever
` (7 preceding siblings ...)
2026-10-19 16:55 ` [PATCH net-next v6 8/9] selftests: tls: Extend coverage for zero-length records Chuck Lever
@ 2026-10-19 16:55 ` Chuck Lever
2026-10-26 22:37 ` [PATCH net-next v6 0/9] net/tls: Receive-path fixes for zero-length data records Jakub Kicinski
9 siblings, 0 replies; 18+ messages in thread
From: Chuck Lever @ 2026-10-19 16:55 UTC (permalink / raw)
To: John Fastabend, Jakub Kicinski, Sabrina Dubroca, David S. Miller,
Paolo Abeni, Simon Horman, Chuck Lever, Dave Watson, Shuah Khan,
Qingfang Deng, Eric Dumazet
Cc: netdev, linux-kselftest
Every existing splice call in this file either passes
SPLICE_F_NONBLOCK or runs on a blocking socket, so nothing
exercises the socket's own O_NONBLOCK on the splice path. A change
that stops consulting sock->file->f_flags puts splice(2) and
sendfile(2) back to sleeping in tls_rx_rec_wait() while a peer
streams empty records, and the suite still reports pass.
Run the zero-length record variants a second time with O_NONBLOCK
set on the socket and SPLICE_F_NONBLOCK left out of the splice
flags. Each variant expects the same result with either flag. A
kernel that ignores the socket flag sleeps in the EAGAIN variant
until the harness timeout rather than returning.
Signed-off-by: Chuck Lever <cel@kernel.org>
---
tools/testing/selftests/net/tls.c | 38 ++++++++++++++++++++++++++++++++------
1 file changed, 32 insertions(+), 6 deletions(-)
diff --git a/tools/testing/selftests/net/tls.c b/tools/testing/selftests/net/tls.c
index aa66d0bbd1b5..cdd53b8f0ed8 100644
--- a/tools/testing/selftests/net/tls.c
+++ b/tools/testing/selftests/net/tls.c
@@ -2829,7 +2829,11 @@ FIXTURE_TEARDOWN(zero_len_splice)
* EAGAIN for a run with no payload behind it, and EINVAL for a control
* record behind the run.
*/
-TEST_F(zero_len_splice, test)
+static void
+zero_len_do_splice(struct __test_metadata *_metadata,
+ FIXTURE_DATA(zero_len_splice) *self,
+ const FIXTURE_VARIANT(zero_len_splice) *variant,
+ unsigned int splice_flags)
{
struct pollfd pfd = { .fd = self->cfd, .events = POLLIN };
const struct raw_rec *payload;
@@ -2837,16 +2841,13 @@ TEST_F(zero_len_splice, test)
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);
+ splice_flags);
EXPECT_EQ(ret, -1);
EXPECT_EQ(errno, -variant->splice_ret);
} else {
@@ -2854,7 +2855,7 @@ TEST_F(zero_len_splice, test)
* below blocks until the harness timeout.
*/
ASSERT_EQ(splice(self->cfd, NULL, p[1], NULL, sizeof(buf),
- SPLICE_F_NONBLOCK), variant->splice_ret);
+ splice_flags), variant->splice_ret);
ret = read(p[0], buf, sizeof(buf));
EXPECT_EQ(ret, variant->splice_ret);
if (ret == variant->splice_ret)
@@ -2869,6 +2870,31 @@ TEST_F(zero_len_splice, test)
close(p[1]);
}
+TEST_F(zero_len_splice, test)
+{
+ if (self->notls)
+ SKIP(return, "no TLS support");
+
+ zero_len_do_splice(_metadata, self, variant, SPLICE_F_NONBLOCK);
+}
+
+/* The socket's O_NONBLOCK governs the record wait, as on a plain TCP
+ * socket, so splice behaves the same with or without SPLICE_F_NONBLOCK.
+ */
+TEST_F(zero_len_splice, nonblock_socket)
+{
+ int sflags;
+
+ if (self->notls)
+ SKIP(return, "no TLS support");
+
+ sflags = fcntl(self->cfd, F_GETFL, 0);
+ ASSERT_GE(sflags, 0);
+ ASSERT_EQ(fcntl(self->cfd, F_SETFL, sflags | O_NONBLOCK), 0);
+
+ zero_len_do_splice(_metadata, self, variant, 0);
+}
+
FIXTURE(tls_err)
{
int fd, cfd;
--
2.55.0
^ permalink raw reply related [flat|nested] 18+ messages in thread* Re: [PATCH net-next v6 0/9] net/tls: Receive-path fixes for zero-length data records
2026-10-19 16:55 [PATCH net-next v6 0/9] net/tls: Receive-path fixes for zero-length data records Chuck Lever
` (8 preceding siblings ...)
2026-10-19 16:55 ` [PATCH net-next v6 9/9] selftests: tls: Cover splice on a nonblocking socket Chuck Lever
@ 2026-10-26 22:37 ` Jakub Kicinski
2026-10-27 13:01 ` Chuck Lever
9 siblings, 1 reply; 18+ messages in thread
From: Jakub Kicinski @ 2026-10-26 22:37 UTC (permalink / raw)
To: Chuck Lever
Cc: John Fastabend, Sabrina Dubroca, David S. Miller, Paolo Abeni,
Simon Horman, Dave Watson, Shuah Khan, Qingfang Deng,
Eric Dumazet, netdev, linux-kselftest, sashiko-bot, sashiko-bot
On Mon, 19 Oct 2026 12:55:16 -0400 Chuck Lever wrote:
> Commit 3be28e2c9cd0 ("net/tls: Consume empty data records in
> tls_sw_read_sock()") fixed one reader. TLS 1.2 and TLS 1.3 both
> permit a zero-length application_data record as a traffic-analysis
> countermeasure (RFC 5246, Section 6.2.1; RFC 8446, Section 5.1), so
> a peer that pads its stream emits them by design. The other two
> software readers still mishandle them. splice(2) reports an empty
> record as EOF, and the caller tears down a connection that is still
> live. recvmsg(2) makes no progress on one, so a peer that streams
> them holds the caller in the kernel past SIGKILL. With MSG_PEEK or
> async decryption, each record is also queued on rx_list, which
> grows without bound.
The shape of this series is still quite confusing to me.
Let me take over a little bit so that we make some progress
(while retaining your authorship where possible).
^ permalink raw reply [flat|nested] 18+ messages in thread* Re: [PATCH net-next v6 0/9] net/tls: Receive-path fixes for zero-length data records
2026-10-26 22:37 ` [PATCH net-next v6 0/9] net/tls: Receive-path fixes for zero-length data records Jakub Kicinski
@ 2026-10-27 13:01 ` Chuck Lever
0 siblings, 0 replies; 18+ messages in thread
From: Chuck Lever @ 2026-10-27 13:01 UTC (permalink / raw)
To: Jakub Kicinski
Cc: John Fastabend, Sabrina Dubroca, David S. Miller, Paolo Abeni,
Simon Horman, Dave Watson, Shuah Khan, Qingfang Deng,
Eric Dumazet, netdev, linux-kselftest, sashiko-bot, sashiko-bot
On Mon, Oct 26, 2026, at 6:37 PM, Jakub Kicinski wrote:
> On Mon, 19 Oct 2026 12:55:16 -0400 Chuck Lever wrote:
>> Commit 3be28e2c9cd0 ("net/tls: Consume empty data records in
>> tls_sw_read_sock()") fixed one reader. TLS 1.2 and TLS 1.3 both
>> permit a zero-length application_data record as a traffic-analysis
>> countermeasure (RFC 5246, Section 6.2.1; RFC 8446, Section 5.1), so
>> a peer that pads its stream emits them by design. The other two
>> software readers still mishandle them. splice(2) reports an empty
>> record as EOF, and the caller tears down a connection that is still
>> live. recvmsg(2) makes no progress on one, so a peer that streams
>> them holds the caller in the kernel past SIGKILL. With MSG_PEEK or
>> async decryption, each record is also queued on rx_list, which
>> grows without bound.
>
> The shape of this series is still quite confusing to me.
> Let me take over a little bit so that we make some progress
> (while retaining your authorship where possible).
Fair enough, thanks! The first 2 patches look fine to me.
I'm not sure whether I need to add an Ack -- they already
carry my SoB.
--
Chuck Lever (Come to NFS bake-a-thon! https://nfsv4bat.org)
^ permalink raw reply [flat|nested] 18+ messages in thread