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. 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 Closes: https://sashiko.dev/#/patchset/20260630191551.875664-1-cel@kernel.org?part=1 Signed-off-by: Chuck Lever --- net/tls/tls_sw.c | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/net/tls/tls_sw.c b/net/tls/tls_sw.c index 0fea14224cec..b7790b17f79c 100644 --- a/net/tls/tls_sw.c +++ b/net/tls/tls_sw.c @@ -1888,8 +1888,8 @@ int tls_sw_recvmsg(struct sock *sk, darg.zc = true; /* Do not use async mode if record is non-data, or if it - * is empty: once a record has gone async, recv_end - * discards the error that ends a run of empty records. + * is empty: 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; @@ -1929,6 +1929,19 @@ int tls_sw_recvmsg(struct sock *sk, nodata = !chunk; tls_rx_rec_done(ctx); + /* Keep an empty record off rx_list. On the zero-copy path + * the strparser owns darg.skb, and tls_rx_rec_done() has + * released it. + */ + 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; + continue; + } + if (!darg.zc) { bool partially_consumed = chunk > len; struct sk_buff *skb = darg.skb; -- 2.55.0