AI Trajectory


Trajectory: (Expand/Collapse All)
Seq Timestamp Type Name Duration
0/0 2026/07/27 12:02 flow patch-triage
0m Args:
null

Results:
{
  "EnableConfigs": [
    "TLS"
  ],
  "FocusSymbols": [
    "tls_sw_recvmsg",
    "tls_sw_splice_read",
    "tls_sw_read_sock"
  ],
  "Reasoning": "The patch modifies the TLS software receive path to properly handle zero-length application_data records (empty data records) in TLS 1.2 and TLS 1.3. These changes affect `recvmsg`, `splice_read`, and `read_sock` operations on TLS sockets, which are reachable from userspace.",
  "WorthFuzzing": true
}

1/1 2026/07/27 12:02 action read-patch-diff
0m Args:
null

Results:
{
  "PatchDiff": "commit 89fc2230b5cd2428099e3cc5e3bb39edeae7bb00\nAuthor: syz-cluster \u003ctriage@syzkaller.com\u003e\nDate:   Mon Jul 27 12:02:32 2026 +0000\n\n    syz-cluster: applied patch under review\n\ndiff --git a/net/tls/tls_sw.c b/net/tls/tls_sw.c\nindex d4afc90fd7966e..0e76b31b12914b 100644\n--- a/net/tls/tls_sw.c\n+++ b/net/tls/tls_sw.c\n@@ -37,6 +37,7 @@\n \n #include \u003clinux/bug.h\u003e\n #include \u003clinux/sched/signal.h\u003e\n+#include \u003clinux/timekeeping.h\u003e\n #include \u003clinux/module.h\u003e\n #include \u003clinux/kernel.h\u003e\n #include \u003clinux/splice.h\u003e\n@@ -1787,6 +1788,24 @@ static void tls_rx_reader_unlock(struct sock *sk, struct tls_sw_context_rx *ctx)\n \trelease_sock(sk);\n }\n \n+/* TLS 1.2 and TLS 1.3 both permit a zero-length application_data\n+ * record as a traffic-analysis countermeasure (RFC 5246, Section\n+ * 6.2.1; RFC 8446, Section 5.1).\n+ */\n+static bool tls_rx_empty_data_rec(int len, unsigned char control)\n+{\n+\treturn !len \u0026\u0026 control == TLS_RECORD_TYPE_DATA;\n+}\n+\n+/* sock_intr_errno() maps the zero timeo of a reader that cannot wait\n+ * to -EINTR, but such a reader has no blocking to interrupt. The rest\n+ * of the receive side reports that case as -EAGAIN.\n+ */\n+static int tls_rx_intr_errno(long timeo)\n+{\n+\treturn timeo ? sock_intr_errno(timeo) : -EAGAIN;\n+}\n+\n int tls_sw_recvmsg(struct sock *sk,\n \t\t   struct msghdr *msg,\n \t\t   size_t len,\n@@ -1858,9 +1877,12 @@ int tls_sw_recvmsg(struct sock *sk,\n \t\t    tlm-\u003econtrol == TLS_RECORD_TYPE_DATA)\n \t\t\tdarg.zc = true;\n \n-\t\t/* Do not use async mode if record is non-data */\n+\t\t/* Do not use async mode if record is non-data, or if it\n+\t\t * is empty: the receive loop frees an empty record's skb,\n+\t\t * so its decryption must have completed.\n+\t\t */\n \t\tif (tlm-\u003econtrol == TLS_RECORD_TYPE_DATA)\n-\t\t\tdarg.async = ctx-\u003easync_capable;\n+\t\t\tdarg.async = ctx-\u003easync_capable \u0026\u0026 to_decrypt;\n \t\telse\n \t\t\tdarg.async = false;\n \n@@ -1896,6 +1918,27 @@ int tls_sw_recvmsg(struct sock *sk,\n \t\tchunk = rxm-\u003efull_len;\n \t\ttls_rx_rec_done(ctx);\n \n+\t\t/* An empty record advances neither loop bound, so a flood\n+\t\t * of them can be interrupted only here. On the zero-copy\n+\t\t * path darg.skb is the strparser anchor, already released\n+\t\t * by tls_rx_rec_done().\n+\t\t */\n+\t\tif (tls_rx_empty_data_rec(chunk, control)) {\n+\t\t\tlong timeo = sock_rcvtimeo(sk, flags \u0026 MSG_DONTWAIT);\n+\n+\t\t\tif (!darg.zc)\n+\t\t\t\tconsume_skb(darg.skb);\n+\n+\t\t\t/* An empty record still marks a boundary. */\n+\t\t\tmsg-\u003emsg_flags |= MSG_EOR;\n+\n+\t\t\tif (signal_pending(current)) {\n+\t\t\t\terr = tls_rx_intr_errno(timeo);\n+\t\t\t\tgoto recv_end;\n+\t\t\t}\n+\t\t\tcontinue;\n+\t\t}\n+\n \t\tif (!darg.zc) {\n \t\t\tbool partially_consumed = chunk \u003e len;\n \t\t\tstruct sk_buff *skb = darg.skb;\n@@ -1990,21 +2033,31 @@ ssize_t tls_sw_splice_read(struct socket *sock,  loff_t *ppos,\n \tstruct sock *sk = sock-\u003esk;\n \tstruct tls_msg *tlm;\n \tstruct sk_buff *skb;\n+\tbool released = true;\n \tssize_t copied = 0;\n+\tbool nonblock;\n \tint chunk;\n \tint err;\n \n-\terr = tls_rx_reader_lock(sk, ctx, flags \u0026 SPLICE_F_NONBLOCK);\n+\tnonblock = (flags \u0026 SPLICE_F_NONBLOCK) ||\n+\t\t   (sock-\u003efile-\u003ef_flags \u0026 O_NONBLOCK);\n+\n+\terr = tls_rx_reader_lock(sk, ctx, nonblock);\n \tif (err \u003c 0)\n \t\treturn err;\n \n+\t/* If crypto failed the connection is broken */\n+\terr = ctx-\u003easync_wait.err;\n+\tif (err)\n+\t\tgoto splice_read_end;\n+\n+retry:\n \tif (!skb_queue_empty(\u0026ctx-\u003erx_list)) {\n \t\tskb = __skb_dequeue(\u0026ctx-\u003erx_list);\n \t} else {\n \t\tstruct tls_decrypt_arg darg;\n \n-\t\terr = tls_rx_rec_wait(sk, flags \u0026 SPLICE_F_NONBLOCK,\n-\t\t\t\t      true, false);\n+\t\terr = tls_rx_rec_wait(sk, nonblock, released, false);\n \t\tif (err \u003c= 0)\n \t\t\tgoto splice_read_end;\n \n@@ -2016,6 +2069,11 @@ ssize_t tls_sw_splice_read(struct socket *sock,  loff_t *ppos,\n \n \t\ttls_rx_rec_done(ctx);\n \t\tskb = darg.skb;\n+\n+\t\t/* The socket lock stays held to the retry, so the\n+\t\t * anchor this wait loaded survives it.\n+\t\t */\n+\t\treleased = false;\n \t}\n \n \trxm = strp_msg(skb);\n@@ -2027,6 +2085,21 @@ ssize_t tls_sw_splice_read(struct socket *sock,  loff_t *ppos,\n \t\tgoto splice_requeue;\n \t}\n \n+\t/* Splicing an empty data record delivers zero bytes, which the\n+\t * caller reads as EOF. tls_rx_rec_wait() skips its signal check\n+\t * while a record is parsed, so test for a signal here.\n+\t */\n+\tif (tls_rx_empty_data_rec(rxm-\u003efull_len, tlm-\u003econtrol)) {\n+\t\tlong timeo = sock_rcvtimeo(sk, nonblock);\n+\n+\t\tconsume_skb(skb);\n+\t\tif (signal_pending(current)) {\n+\t\t\terr = tls_rx_intr_errno(timeo);\n+\t\t\tgoto splice_read_end;\n+\t\t}\n+\t\tgoto retry;\n+\t}\n+\n \tchunk = min_t(unsigned int, rxm-\u003efull_len, len);\n \tcopied = skb_splice_bits(skb, sk, rxm-\u003eoffset, pipe, chunk, flags);\n \tif (copied \u003c 0)\n@@ -2049,6 +2122,11 @@ ssize_t tls_sw_splice_read(struct socket *sock,  loff_t *ppos,\n \tgoto splice_read_end;\n }\n \n+/* Bound the time that consecutive empty ingress data records keep\n+ * the socket lock held without releasing it.\n+ */\n+#define TLS_RX_NODATA_NS NSEC_PER_MSEC\n+\n int tls_sw_read_sock(struct sock *sk, read_descriptor_t *desc,\n \t\t     sk_read_actor_t read_actor)\n {\n@@ -2057,6 +2135,7 @@ int tls_sw_read_sock(struct sock *sk, read_descriptor_t *desc,\n \tstruct tls_prot_info *prot = \u0026tls_ctx-\u003eprot_info;\n \tstruct strp_msg *rxm = NULL;\n \tstruct sk_buff *skb = NULL;\n+\tu64 nodata_deadline = 0;\n \tstruct sk_psock *psock;\n \tsize_t flushed_at = 0;\n \tbool released = true;\n@@ -2115,14 +2194,25 @@ int tls_sw_read_sock(struct sock *sk, read_descriptor_t *desc,\n \t\t\tgoto read_sock_requeue;\n \t\t}\n \n-\t\t/* An empty data record (legal in TLS 1.3) gives a zero\n-\t\t * read_actor return, indistinguishable from the consumer\n-\t\t * stalling; the used \u003c= 0 path would requeue it at the\n-\t\t * head of rx_list and block all later records. Consume it\n-\t\t * here instead.\n+\t\t/* An empty data record gives a zero read_actor return,\n+\t\t * indistinguishable from the consumer stalling; the\n+\t\t * used \u003c= 0 path would requeue it at the head of rx_list\n+\t\t * and block all later records. Consume it here instead.\n \t\t */\n-\t\tif (rxm-\u003efull_len == 0) {\n+\t\tif (tls_rx_empty_data_rec(rxm-\u003efull_len, tlm-\u003econtrol)) {\n+\t\t\terr = 0;\n \t\t\tconsume_skb(skb);\n+\t\t\tif (!nodata_deadline) {\n+\t\t\t\tnodata_deadline = ktime_get_ns() +\n+\t\t\t\t\t\t  TLS_RX_NODATA_NS;\n+\t\t\t} else if (ktime_get_ns() \u003e= nodata_deadline) {\n+\t\t\t\t/* Queued records raise no new sk_data_ready(),\n+\t\t\t\t * and tls_rx_reader_release() announces only to\n+\t\t\t\t * saved_data_ready(), not the consumer's own.\n+\t\t\t\t */\n+\t\t\t\tsk-\u003esk_data_ready(sk);\n+\t\t\t\tbreak;\n+\t\t\t}\n \t\t\tcontinue;\n \t\t}\n \n@@ -2133,6 +2223,7 @@ int tls_sw_read_sock(struct sock *sk, read_descriptor_t *desc,\n \t\t\tgoto read_sock_requeue;\n \t\t}\n \t\tcopied += used;\n+\t\tnodata_deadline = 0;\n \t\tif (used \u003c rxm-\u003efull_len) {\n \t\t\trxm-\u003eoffset += used;\n \t\t\trxm-\u003efull_len -= used;\ndiff --git a/tools/testing/selftests/net/tls.c b/tools/testing/selftests/net/tls.c\nindex cbdd3ea28b998f..8b68dbfcc59246 100644\n--- a/tools/testing/selftests/net/tls.c\n+++ b/tools/testing/selftests/net/tls.c\n@@ -24,6 +24,7 @@\n #include \"kselftest_harness.h\"\n \n #define TLS_PAYLOAD_MAX_LEN 16384\n+#define TLS_HDR_LEN 5\n #define SOL_TLS 282\n \n static int fips_enabled;\n@@ -2544,6 +2545,9 @@ TEST_F(zero_len, test)\n \tint rec_off;\n \tint i;\n \n+\tif (self-\u003enotls)\n+\t\tSKIP(return, \"no TLS support\");\n+\n \tfor (i = 0; i \u003c 4 \u0026\u0026 variant-\u003erecs[i]; i++)\n \t\tEXPECT_EQ(send(self-\u003efd, variant-\u003erecs[i]-\u003ecipher_data,\n \t\t\t       variant-\u003erecs[i]-\u003ecipher_len, 0),\n@@ -2573,6 +2577,248 @@ TEST_F(zero_len, test)\n \t}\n };\n \n+static void zero_len_sock_pair(struct __test_metadata *_metadata,\n+\t\t\t       int *fd, int *cfd, bool *notls)\n+{\n+\tstruct tls_crypto_info_keys tls12;\n+\tint ret;\n+\n+\ttls_crypto_info_init(TLS_1_2_VERSION, TLS_CIPHER_AES_CCM_128,\n+\t\t\t     \u0026tls12, 0);\n+\n+\tulp_sock_pair(_metadata, fd, cfd, notls);\n+\tif (*notls)\n+\t\treturn;\n+\n+\t/* fd stays keyless; these fixtures send raw records over it */\n+\tret = setsockopt(*cfd, SOL_TLS, TLS_RX, \u0026tls12, tls12.len);\n+\tASSERT_EQ(ret, 0);\n+}\n+\n+/* Send a variant's records; return the last one carrying payload */\n+static const struct raw_rec *\n+zero_len_send_recs(struct __test_metadata *_metadata, int fd,\n+\t\t   const struct raw_rec *const *recs)\n+{\n+\tconst struct raw_rec *payload = NULL;\n+\tint i;\n+\n+\tfor (i = 0; i \u003c 4 \u0026\u0026 recs[i]; i++) {\n+\t\tEXPECT_EQ(send(fd, recs[i]-\u003ecipher_data, recs[i]-\u003ecipher_len, 0),\n+\t\t\t  recs[i]-\u003ecipher_len);\n+\t\tif (recs[i]-\u003eplain_len)\n+\t\t\tpayload = recs[i];\n+\t}\n+\n+\treturn payload;\n+}\n+\n+FIXTURE(zero_len_peek)\n+{\n+\tint fd, cfd;\n+\tbool notls;\n+};\n+\n+FIXTURE_VARIANT(zero_len_peek)\n+{\n+\tconst struct raw_rec *recs[4];\n+\tssize_t peek_ret;\n+};\n+\n+FIXTURE_VARIANT_ADD(zero_len_peek, 0data_0data_data)\n+{\n+\t.recs = { \u0026id0_data_l0, \u0026id1_data_l0, \u0026id2_data_l11, },\n+\t.peek_ret = 11,\n+};\n+\n+FIXTURE_VARIANT_ADD(zero_len_peek, 0data_0data_0data)\n+{\n+\t.recs = { \u0026id0_data_l0, \u0026id1_data_l0, \u0026id2_data_l0, },\n+\t.peek_ret = -EAGAIN,\n+};\n+\n+FIXTURE_SETUP(zero_len_peek)\n+{\n+\tzero_len_sock_pair(_metadata, \u0026self-\u003efd, \u0026self-\u003ecfd, \u0026self-\u003enotls);\n+}\n+\n+FIXTURE_TEARDOWN(zero_len_peek)\n+{\n+\tclose(self-\u003efd);\n+\tclose(self-\u003ecfd);\n+}\n+\n+/* Peeking past a run of empty data records must reach the payload\n+ * behind them, and a run with no payload behind it must report EAGAIN\n+ * rather than the zero return that means EOF.\n+ */\n+TEST_F(zero_len_peek, test)\n+{\n+\tconst struct raw_rec *payload;\n+\tunsigned char buf[128];\n+\tssize_t ret;\n+\n+\tif (self-\u003enotls)\n+\t\tSKIP(return, \"no TLS support\");\n+\n+\tpayload = zero_len_send_recs(_metadata, self-\u003efd, variant-\u003erecs);\n+\n+\tif (variant-\u003epeek_ret \u003c 0) {\n+\t\tret = recv(self-\u003ecfd, buf, sizeof(buf),\n+\t\t\t   MSG_DONTWAIT | MSG_PEEK);\n+\t\tEXPECT_EQ(ret, -1);\n+\t\tif (ret == -1)\n+\t\t\tEXPECT_EQ(errno, -variant-\u003epeek_ret);\n+\t\treturn;\n+\t}\n+\n+\tret = recv(self-\u003ecfd, buf, sizeof(buf), MSG_DONTWAIT | MSG_PEEK);\n+\tEXPECT_EQ(ret, variant-\u003epeek_ret);\n+\tif (ret == variant-\u003epeek_ret)\n+\t\tEXPECT_EQ(memcmp(buf, payload-\u003eplain_data,\n+\t\t\t\t variant-\u003epeek_ret), 0);\n+\n+\t/* Peeking left the payload in place for the read that follows */\n+\tret = recv(self-\u003ecfd, buf, sizeof(buf), MSG_DONTWAIT);\n+\tEXPECT_EQ(ret, variant-\u003epeek_ret);\n+\tif (ret == variant-\u003epeek_ret)\n+\t\tEXPECT_EQ(memcmp(buf, payload-\u003eplain_data,\n+\t\t\t\t variant-\u003epeek_ret), 0);\n+\n+\tret = recv(self-\u003ecfd, buf, sizeof(buf), MSG_DONTWAIT);\n+\tEXPECT_EQ(ret, -1);\n+\tif (ret == -1)\n+\t\tEXPECT_EQ(errno, EAGAIN);\n+}\n+\n+FIXTURE(zero_len_splice)\n+{\n+\tint fd, cfd;\n+\tbool notls;\n+};\n+\n+FIXTURE_VARIANT(zero_len_splice)\n+{\n+\tconst struct raw_rec *recs[4];\n+\tssize_t splice_ret;\n+};\n+\n+FIXTURE_VARIANT_ADD(zero_len_splice, 0data_data)\n+{\n+\t.recs = { \u0026id0_data_l0, \u0026id1_data_l11, },\n+\t.splice_ret = 11,\n+};\n+\n+FIXTURE_VARIANT_ADD(zero_len_splice, 0data_0data_data)\n+{\n+\t.recs = { \u0026id0_data_l0, \u0026id1_data_l0, \u0026id2_data_l11, },\n+\t.splice_ret = 11,\n+};\n+\n+FIXTURE_VARIANT_ADD(zero_len_splice, 0data_0data_0data)\n+{\n+\t.recs = { \u0026id0_data_l0, \u0026id1_data_l0, \u0026id2_data_l0, },\n+\t.splice_ret = -EAGAIN,\n+};\n+\n+FIXTURE_VARIANT_ADD(zero_len_splice, 0data_0ctrl)\n+{\n+\t.recs = { \u0026id0_data_l0, \u0026id1_ctrl_l0, },\n+\t.splice_ret = -EINVAL,\n+};\n+\n+FIXTURE_SETUP(zero_len_splice)\n+{\n+\tzero_len_sock_pair(_metadata, \u0026self-\u003efd, \u0026self-\u003ecfd, \u0026self-\u003enotls);\n+}\n+\n+FIXTURE_TEARDOWN(zero_len_splice)\n+{\n+\tclose(self-\u003efd);\n+\tclose(self-\u003ecfd);\n+}\n+\n+/* An empty data record splices zero bytes, which a splice caller reads\n+ * as EOF. Splicing must skip past such a record to the payload behind\n+ * it, and report EAGAIN when a run of them has no payload behind it.\n+ * A control record behind the run reports EINVAL, the error splice\n+ * already reports for a control record it meets first.\n+ */\n+static void\n+zero_len_do_splice(struct __test_metadata *_metadata,\n+\t\t   struct _test_data_zero_len_splice *self,\n+\t\t   const struct _fixture_variant_zero_len_splice *variant,\n+\t\t   unsigned int splice_flags)\n+{\n+\tconst struct raw_rec *payload;\n+\tunsigned char buf[128];\n+\tssize_t ret;\n+\tint p[2];\n+\n+\tASSERT_GE(pipe(p), 0);\n+\n+\tpayload = zero_len_send_recs(_metadata, self-\u003efd, variant-\u003erecs);\n+\n+\tif (variant-\u003esplice_ret \u003c 0) {\n+\t\tret = splice(self-\u003ecfd, NULL, p[1], NULL, sizeof(buf),\n+\t\t\t     splice_flags);\n+\t\tEXPECT_EQ(ret, -1);\n+\t\tif (ret == -1)\n+\t\t\tEXPECT_EQ(errno, -variant-\u003esplice_ret);\n+\t} else {\n+\t\t/* Assert: a zero return, which is what an unfixed kernel\n+\t\t * gives here, leaves the pipe empty, and the read below\n+\t\t * would then block until the harness timeout.\n+\t\t */\n+\t\tASSERT_EQ(splice(self-\u003ecfd, NULL, p[1], NULL, sizeof(buf),\n+\t\t\t\t splice_flags), variant-\u003esplice_ret);\n+\t\tret = read(p[0], buf, sizeof(buf));\n+\t\tEXPECT_EQ(ret, variant-\u003esplice_ret);\n+\t\tif (ret == variant-\u003esplice_ret)\n+\t\t\tEXPECT_EQ(memcmp(buf, payload-\u003eplain_data,\n+\t\t\t\t\t variant-\u003esplice_ret), 0);\n+\n+\t\t/* Reaching the payload consumed the empty records ahead\n+\t\t * of it rather than leaving them on the receive queue\n+\t\t */\n+\t\tret = recv(self-\u003ecfd, buf, sizeof(buf), MSG_DONTWAIT);\n+\t\tEXPECT_EQ(ret, -1);\n+\t\tif (ret == -1)\n+\t\t\tEXPECT_EQ(errno, EAGAIN);\n+\t}\n+\n+\tclose(p[0]);\n+\tclose(p[1]);\n+}\n+\n+TEST_F(zero_len_splice, test)\n+{\n+\tif (self-\u003enotls)\n+\t\tSKIP(return, \"no TLS support\");\n+\n+\tzero_len_do_splice(_metadata, self, variant, SPLICE_F_NONBLOCK);\n+}\n+\n+/* The socket's own O_NONBLOCK governs the record wait, as it does on a\n+ * plain TCP socket, so a splice that omits SPLICE_F_NONBLOCK reaches\n+ * the same outcome as one that sets it. An unfixed kernel derives the\n+ * wait from SPLICE_F_NONBLOCK alone and sleeps here until the harness\n+ * timeout.\n+ */\n+TEST_F(zero_len_splice, nonblock_socket)\n+{\n+\tint sflags;\n+\n+\tif (self-\u003enotls)\n+\t\tSKIP(return, \"no TLS support\");\n+\n+\tsflags = fcntl(self-\u003ecfd, F_GETFL, 0);\n+\tASSERT_GE(sflags, 0);\n+\tASSERT_EQ(fcntl(self-\u003ecfd, F_SETFL, sflags | O_NONBLOCK), 0);\n+\n+\tzero_len_do_splice(_metadata, self, variant, 0);\n+}\n+\n FIXTURE(tls_err)\n {\n \tint fd, cfd;\n@@ -2638,28 +2884,85 @@ TEST_F(tls_err, bad_rec)\n \tEXPECT_EQ(errno, EAGAIN);\n }\n \n+/* Encrypt a record on the TX-only socket pair, corrupt its last\n+ * byte, and hand the result to the socket under test. cfd carries a\n+ * byte stream, so one recv() can return part of a record: take the\n+ * fragment length from the record header and wait for the remainder.\n+ */\n+static void tls_send_bad_auth(struct __test_metadata *_metadata,\n+\t\t\t      int fd, int cfd, int fd2)\n+{\n+\tchar buf[128];\n+\tint len;\n+\n+\tmemrnd(buf, sizeof(buf) / 2);\n+\tASSERT_EQ(send(fd, buf, sizeof(buf) / 2, 0), sizeof(buf) / 2);\n+\n+\tASSERT_EQ(recv(cfd, buf, TLS_HDR_LEN, MSG_WAITALL), TLS_HDR_LEN);\n+\n+\tlen = ((unsigned char)buf[3] \u003c\u003c 8) | (unsigned char)buf[4];\n+\tASSERT_GT(len, 0);\n+\tASSERT_LE(len, (int)sizeof(buf) - TLS_HDR_LEN);\n+\n+\tASSERT_EQ(recv(cfd, buf + TLS_HDR_LEN, len, MSG_WAITALL), len);\n+\n+\tbuf[TLS_HDR_LEN + len - 1]++;\n+\n+\tASSERT_EQ(send(fd2, buf, TLS_HDR_LEN + len, 0), TLS_HDR_LEN + len);\n+}\n+\n TEST_F(tls_err, bad_auth)\n {\n \tchar buf[128];\n-\tint n;\n \n \tif (self-\u003enotls)\n \t\tSKIP(return, \"no TLS support\");\n \n-\tmemrnd(buf, sizeof(buf) / 2);\n-\tEXPECT_EQ(send(self-\u003efd, buf, sizeof(buf) / 2, 0), sizeof(buf) / 2);\n-\tn = recv(self-\u003ecfd, buf, sizeof(buf), 0);\n-\tEXPECT_GT(n, sizeof(buf) / 2);\n-\n-\tbuf[n - 1]++;\n+\ttls_send_bad_auth(_metadata, self-\u003efd, self-\u003ecfd, self-\u003efd2);\n \n-\tEXPECT_EQ(send(self-\u003efd2, buf, n, 0), n);\n \tEXPECT_EQ(recv(self-\u003ecfd2, buf, sizeof(buf), 0), -1);\n \tEXPECT_EQ(errno, EBADMSG);\n \tEXPECT_EQ(recv(self-\u003ecfd2, buf, sizeof(buf), 0), -1);\n \tEXPECT_EQ(errno, EBADMSG);\n }\n \n+/* A record that did not authenticate breaks the connection for every\n+ * reader, splice included.\n+ *\n+ * The two decrypt paths reach that result differently. A synchronous\n+ * decrypt leaves the record parsed, so the splice re-runs the decrypt\n+ * and fails on the record itself; the ctx-\u003easync_wait.err check in\n+ * tls_sw_splice_read() is not what stops it. Only an asynchronous\n+ * decrypt, which needs a TLS 1.2 socket and an AEAD advertising\n+ * CRYPTO_ALG_ASYNC, consumes the record before the failure is\n+ * recorded, leaving that check the sole reason the splice fails.\n+ */\n+TEST_F(tls_err, bad_auth_splice)\n+{\n+\tchar buf[128];\n+\tssize_t ret;\n+\tint p[2];\n+\n+\tif (self-\u003enotls)\n+\t\tSKIP(return, \"no TLS support\");\n+\n+\ttls_send_bad_auth(_metadata, self-\u003efd, self-\u003ecfd, self-\u003efd2);\n+\n+\tEXPECT_EQ(recv(self-\u003ecfd2, buf, sizeof(buf), 0), -1);\n+\tEXPECT_EQ(errno, EBADMSG);\n+\n+\tASSERT_GE(pipe(p), 0);\n+\n+\tret = splice(self-\u003ecfd2, NULL, p[1], NULL, sizeof(buf),\n+\t\t     SPLICE_F_NONBLOCK);\n+\tEXPECT_EQ(ret, -1);\n+\tif (ret == -1)\n+\t\tEXPECT_EQ(errno, EBADMSG);\n+\n+\tclose(p[0]);\n+\tclose(p[1]);\n+}\n+\n TEST_F(tls_err, bad_in_large_read)\n {\n \tchar txt[3][64];\n@@ -2915,7 +3218,6 @@ static size_t parse_tls_records(struct __test_metadata *_metadata,\n {\n \tconst __u8 *rec = rx_buf;\n \tsize_t total_plaintext_rx = 0;\n-\tconst __u8 rec_header_len = 5;\n \n \twhile (rec \u003c rx_buf + rx_len) {\n \t\t__u16 record_payload_len;\n@@ -2935,7 +3237,7 @@ static size_t parse_tls_records(struct __test_metadata *_metadata,\n \n \t\t/* Plaintext must not exceed the specified limit */\n \t\tASSERT_LE(plaintext_len, max_payload_len);\n-\t\trec += rec_header_len + record_payload_len;\n+\t\trec += TLS_HDR_LEN + record_payload_len;\n \t}\n \n \treturn total_plaintext_rx;\n"
}

2/1 2026/07/27 12:02 agent patch-evaluator
0m Model:
best-expensive

Args:
null

Results:
{
  "EnableConfigs": [
    "TLS"
  ],
  "FocusSymbols": [
    "tls_sw_recvmsg",
    "tls_sw_splice_read",
    "tls_sw_read_sock"
  ],
  "Reasoning": "The patch modifies the TLS software receive path to properly handle zero-length application_data records (empty data records) in TLS 1.2 and TLS 1.3. These changes affect `recvmsg`, `splice_read`, and `read_sock` operations on TLS sockets, which are reachable from userspace.",
  "WorthFuzzing": true
}

Instruction:
You are an expert Linux kernel maintainer.
Your job is to review a provided patch series and determine
if it makes functional changes to the kernel that should be fuzzed.

IMPORTANT: The changes have ALREADY been applied and committed as the HEAD commit in
your workspace. Do NOT rely on your internal knowledge of the kernel. You must actively
use your code access tools to examine the actual source code and confirm any assumptions.

Return WorthFuzzing=false if the patch only contains:
- Modifications to Documentation/, Kconfig files, or code comments.
- Purely decorative changes, such as logging (e.g., pr_err, printk) or tracepoints.
- Changes to numeric constants or macros that do not functionally alter execution flow.
- Code paths that are impossible to reach in virtualized environments like GCE or QEMU,
  even when utilizing software-emulated hardware (e.g., usb gadget, mac80211_hwsim).
- Code in vendor-specific PCIe switch, SmartNIC, or GPU drivers (e.g., mlxsw, pds_core, qed,
  ionic, amdgpu) that require physical PCIe hardware cards not emulated in standard QEMU.
- Driver .remove, .shutdown, or pci_unregister_driver teardown callbacks (e.g., igb_remove)
  that are executed only during PCI hot-unplug or sysfs driver unbind operations.

If it modifies reachable core kernel logic, drivers, or architectures, use your code search
tools to verify the code can be executed, then return WorthFuzzing=true.

When returning WorthFuzzing=true, you MUST ALSO:
1. Extract any specific kernel functions that should be heavily fuzzed into FocusSymbols.
   Avoid listing generic hot-path functions to prevent skewed test distributions.
   Prefer non-static, non-inlined API entrypoint functions over internal static helper functions
   (which are inlined by the compiler and do not have distinct symbol addresses).
2. Identify any specific CONFIG_ options required to properly test this new/modified feature.
   Go and look into the Kconfig files and check for ifdefs around the code, do not make assumptions.
   Also check "depends on" lines in Kconfig to include any non-standard parent subsystem configs
   needed for Kbuild to compile the code statically into vmlinux. List them in the EnableConfigs
   output array, and DO NOT add a 'CONFIG_' prefix (e.g., return "NET_IPV4" instead of "CONFIG_NET_IPV4").
Prefer calling several tools at the same time to save round-trips.


Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.

Prompt:
For your convenience, here is the diff of the changes:
commit 89fc2230b5cd2428099e3cc5e3bb39edeae7bb00
Author: syz-cluster <triage@syzkaller.com>
Date:   Mon Jul 27 12:02:32 2026 +0000

    syz-cluster: applied patch under review

diff --git a/net/tls/tls_sw.c b/net/tls/tls_sw.c
index d4afc90fd7966e..0e76b31b12914b 100644
--- a/net/tls/tls_sw.c
+++ b/net/tls/tls_sw.c
@@ -37,6 +37,7 @@
 
 #include <linux/bug.h>
 #include <linux/sched/signal.h>
+#include <linux/timekeeping.h>
 #include <linux/module.h>
 #include <linux/kernel.h>
 #include <linux/splice.h>
@@ -1787,6 +1788,24 @@ static void tls_rx_reader_unlock(struct sock *sk, struct tls_sw_context_rx *ctx)
 	release_sock(sk);
 }
 
+/* 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).
+ */
+static bool tls_rx_empty_data_rec(int len, unsigned char control)
+{
+	return !len && control == TLS_RECORD_TYPE_DATA;
+}
+
+/* sock_intr_errno() maps the zero timeo of a reader that cannot wait
+ * to -EINTR, but such a reader has no blocking to interrupt. The rest
+ * of the receive side reports that case as -EAGAIN.
+ */
+static int tls_rx_intr_errno(long timeo)
+{
+	return timeo ? sock_intr_errno(timeo) : -EAGAIN;
+}
+
 int tls_sw_recvmsg(struct sock *sk,
 		   struct msghdr *msg,
 		   size_t len,
@@ -1858,9 +1877,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 if it
+		 * is empty: the receive loop frees an empty record's skb,
+		 * so its decryption must have completed.
+		 */
 		if (tlm->control == TLS_RECORD_TYPE_DATA)
-			darg.async = ctx->async_capable;
+			darg.async = ctx->async_capable && to_decrypt;
 		else
 			darg.async = false;
 
@@ -1896,6 +1918,27 @@ int tls_sw_recvmsg(struct sock *sk,
 		chunk = rxm->full_len;
 		tls_rx_rec_done(ctx);
 
+		/* An empty record advances neither loop bound, so a flood
+		 * of them can be interrupted only here. On the zero-copy
+		 * path darg.skb is the strparser anchor, already released
+		 * by tls_rx_rec_done().
+		 */
+		if (tls_rx_empty_data_rec(chunk, control)) {
+			long timeo = sock_rcvtimeo(sk, flags & MSG_DONTWAIT);
+
+			if (!darg.zc)
+				consume_skb(darg.skb);
+
+			/* An empty record still marks a boundary. */
+			msg->msg_flags |= MSG_EOR;
+
+			if (signal_pending(current)) {
+				err = tls_rx_intr_errno(timeo);
+				goto recv_end;
+			}
+			continue;
+		}
+
 		if (!darg.zc) {
 			bool partially_consumed = chunk > len;
 			struct sk_buff *skb = darg.skb;
@@ -1990,21 +2033,31 @@ 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;
 	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;
 
+	/* If crypto failed the connection is broken */
+	err = ctx->async_wait.err;
+	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, flags & SPLICE_F_NONBLOCK,
-				      true, false);
+		err = tls_rx_rec_wait(sk, nonblock, released, false);
 		if (err <= 0)
 			goto splice_read_end;
 
@@ -2016,6 +2069,11 @@ ssize_t tls_sw_splice_read(struct socket *sock,  loff_t *ppos,
 
 		tls_rx_rec_done(ctx);
 		skb = darg.skb;
+
+		/* The socket lock stays held to the retry, so the
+		 * anchor this wait loaded survives it.
+		 */
+		released = false;
 	}
 
 	rxm = strp_msg(skb);
@@ -2027,6 +2085,21 @@ ssize_t tls_sw_splice_read(struct socket *sock,  loff_t *ppos,
 		goto splice_requeue;
 	}
 
+	/* Splicing an empty data record delivers zero bytes, which the
+	 * caller reads as EOF. tls_rx_rec_wait() skips its signal check
+	 * while a record is parsed, so test for a signal here.
+	 */
+	if (tls_rx_empty_data_rec(rxm->full_len, tlm->control)) {
+		long timeo = sock_rcvtimeo(sk, nonblock);
+
+		consume_skb(skb);
+		if (signal_pending(current)) {
+			err = tls_rx_intr_errno(timeo);
+			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)
@@ -2049,6 +2122,11 @@ ssize_t tls_sw_splice_read(struct socket *sock,  loff_t *ppos,
 	goto splice_read_end;
 }
 
+/* Bound the time that consecutive empty ingress data records keep
+ * the socket lock held without releasing it.
+ */
+#define TLS_RX_NODATA_NS NSEC_PER_MSEC
+
 int tls_sw_read_sock(struct sock *sk, read_descriptor_t *desc,
 		     sk_read_actor_t read_actor)
 {
@@ -2057,6 +2135,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;
+	u64 nodata_deadline = 0;
 	struct sk_psock *psock;
 	size_t flushed_at = 0;
 	bool released = true;
@@ -2115,14 +2194,25 @@ int tls_sw_read_sock(struct sock *sk, read_descriptor_t *desc,
 			goto read_sock_requeue;
 		}
 
-		/* An empty data record (legal in TLS 1.3) gives a zero
-		 * read_actor return, indistinguishable from the consumer
-		 * stalling; the used <= 0 path would requeue it at the
-		 * head of rx_list and block all later records. Consume it
-		 * here instead.
+		/* An empty data record gives a zero read_actor return,
+		 * indistinguishable from the consumer stalling; the
+		 * used <= 0 path would requeue it at the head of rx_list
+		 * and block all later records. Consume it here instead.
 		 */
-		if (rxm->full_len == 0) {
+		if (tls_rx_empty_data_rec(rxm->full_len, tlm->control)) {
+			err = 0;
 			consume_skb(skb);
+			if (!nodata_deadline) {
+				nodata_deadline = ktime_get_ns() +
+						  TLS_RX_NODATA_NS;
+			} else if (ktime_get_ns() >= nodata_deadline) {
+				/* Queued records raise no new sk_data_ready(),
+				 * and tls_rx_reader_release() announces only to
+				 * saved_data_ready(), not the consumer's own.
+				 */
+				sk->sk_data_ready(sk);
+				break;
+			}
 			continue;
 		}
 
@@ -2133,6 +2223,7 @@ int tls_sw_read_sock(struct sock *sk, read_descriptor_t *desc,
 			goto read_sock_requeue;
 		}
 		copied += used;
+		nodata_deadline = 0;
 		if (used < rxm->full_len) {
 			rxm->offset += used;
 			rxm->full_len -= used;
diff --git a/tools/testing/selftests/net/tls.c b/tools/testing/selftests/net/tls.c
index cbdd3ea28b998f..8b68dbfcc59246 100644
--- a/tools/testing/selftests/net/tls.c
+++ b/tools/testing/selftests/net/tls.c
@@ -24,6 +24,7 @@
 #include "kselftest_harness.h"
 
 #define TLS_PAYLOAD_MAX_LEN 16384
+#define TLS_HDR_LEN 5
 #define SOL_TLS 282
 
 static int fips_enabled;
@@ -2544,6 +2545,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),
@@ -2573,6 +2577,248 @@ TEST_F(zero_len, test)
 	}
 };
 
+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 stays keyless; 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; 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_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
+ * rather than the zero return that means EOF.
+ */
+TEST_F(zero_len_peek, test)
+{
+	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);
+		if (ret == -1)
+			EXPECT_EQ(errno, -variant->peek_ret);
+		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);
+
+	/* Peeking left the payload in place for the read that follows */
+	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);
+	if (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);
+}
+
+/* An empty data record splices zero bytes, which a splice caller reads
+ * as EOF. Splicing must skip past such a record to the payload behind
+ * it, and report EAGAIN when a run of them has no payload behind it.
+ * A control record behind the run reports EINVAL, the error splice
+ * already reports for a control record it meets first.
+ */
+static void
+zero_len_do_splice(struct __test_metadata *_metadata,
+		   struct _test_data_zero_len_splice *self,
+		   const struct _fixture_variant_zero_len_splice *variant,
+		   unsigned int splice_flags)
+{
+	const struct raw_rec *payload;
+	unsigned char buf[128];
+	ssize_t ret;
+	int p[2];
+
+	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_flags);
+		EXPECT_EQ(ret, -1);
+		if (ret == -1)
+			EXPECT_EQ(errno, -variant->splice_ret);
+	} else {
+		/* Assert: a zero return, which is what an unfixed kernel
+		 * gives here, leaves the pipe empty, and the read below
+		 * would then block until the harness timeout.
+		 */
+		ASSERT_EQ(splice(self->cfd, NULL, p[1], NULL, sizeof(buf),
+				 splice_flags), 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);
+
+		/* Reaching the payload consumed the empty records ahead
+		 * of it rather than leaving them on the receive queue
+		 */
+		ret = recv(self->cfd, buf, sizeof(buf), MSG_DONTWAIT);
+		EXPECT_EQ(ret, -1);
+		if (ret == -1)
+			EXPECT_EQ(errno, EAGAIN);
+	}
+
+	close(p[0]);
+	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 own O_NONBLOCK governs the record wait, as it does on a
+ * plain TCP socket, so a splice that omits SPLICE_F_NONBLOCK reaches
+ * the same outcome as one that sets it. An unfixed kernel derives the
+ * wait from SPLICE_F_NONBLOCK alone and sleeps here until the harness
+ * timeout.
+ */
+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;
@@ -2638,28 +2884,85 @@ TEST_F(tls_err, bad_rec)
 	EXPECT_EQ(errno, EAGAIN);
 }
 
+/* Encrypt a record on the TX-only socket pair, corrupt its last
+ * byte, and hand the result to the socket under test. cfd carries a
+ * byte stream, so one recv() can return part of a record: take the
+ * fragment length from the record header and wait for the remainder.
+ */
+static void tls_send_bad_auth(struct __test_metadata *_metadata,
+			      int fd, int cfd, int fd2)
+{
+	char buf[128];
+	int len;
+
+	memrnd(buf, sizeof(buf) / 2);
+	ASSERT_EQ(send(fd, buf, sizeof(buf) / 2, 0), sizeof(buf) / 2);
+
+	ASSERT_EQ(recv(cfd, buf, TLS_HDR_LEN, MSG_WAITALL), TLS_HDR_LEN);
+
+	len = ((unsigned char)buf[3] << 8) | (unsigned char)buf[4];
+	ASSERT_GT(len, 0);
+	ASSERT_LE(len, (int)sizeof(buf) - TLS_HDR_LEN);
+
+	ASSERT_EQ(recv(cfd, buf + TLS_HDR_LEN, len, MSG_WAITALL), len);
+
+	buf[TLS_HDR_LEN + len - 1]++;
+
+	ASSERT_EQ(send(fd2, buf, TLS_HDR_LEN + len, 0), TLS_HDR_LEN + len);
+}
+
 TEST_F(tls_err, bad_auth)
 {
 	char buf[128];
-	int n;
 
 	if (self->notls)
 		SKIP(return, "no TLS support");
 
-	memrnd(buf, sizeof(buf) / 2);
-	EXPECT_EQ(send(self->fd, buf, sizeof(buf) / 2, 0), sizeof(buf) / 2);
-	n = recv(self->cfd, buf, sizeof(buf), 0);
-	EXPECT_GT(n, sizeof(buf) / 2);
-
-	buf[n - 1]++;
+	tls_send_bad_auth(_metadata, self->fd, self->cfd, self->fd2);
 
-	EXPECT_EQ(send(self->fd2, buf, n, 0), n);
 	EXPECT_EQ(recv(self->cfd2, buf, sizeof(buf), 0), -1);
 	EXPECT_EQ(errno, EBADMSG);
 	EXPECT_EQ(recv(self->cfd2, buf, sizeof(buf), 0), -1);
 	EXPECT_EQ(errno, EBADMSG);
 }
 
+/* A record that did not authenticate breaks the connection for every
+ * reader, splice included.
+ *
+ * The two decrypt paths reach that result differently. A synchronous
+ * decrypt leaves the record parsed, so the splice re-runs the decrypt
+ * and fails on the record itself; the ctx->async_wait.err check in
+ * tls_sw_splice_read() is not what stops it. Only an asynchronous
+ * decrypt, which needs a TLS 1.2 socket and an AEAD advertising
+ * CRYPTO_ALG_ASYNC, consumes the record before the failure is
+ * recorded, leaving that check the sole reason the splice fails.
+ */
+TEST_F(tls_err, bad_auth_splice)
+{
+	char buf[128];
+	ssize_t ret;
+	int p[2];
+
+	if (self->notls)
+		SKIP(return, "no TLS support");
+
+	tls_send_bad_auth(_metadata, self->fd, self->cfd, self->fd2);
+
+	EXPECT_EQ(recv(self->cfd2, buf, sizeof(buf), 0), -1);
+	EXPECT_EQ(errno, EBADMSG);
+
+	ASSERT_GE(pipe(p), 0);
+
+	ret = splice(self->cfd2, NULL, p[1], NULL, sizeof(buf),
+		     SPLICE_F_NONBLOCK);
+	EXPECT_EQ(ret, -1);
+	if (ret == -1)
+		EXPECT_EQ(errno, EBADMSG);
+
+	close(p[0]);
+	close(p[1]);
+}
+
 TEST_F(tls_err, bad_in_large_read)
 {
 	char txt[3][64];
@@ -2915,7 +3218,6 @@ static size_t parse_tls_records(struct __test_metadata *_metadata,
 {
 	const __u8 *rec = rx_buf;
 	size_t total_plaintext_rx = 0;
-	const __u8 rec_header_len = 5;
 
 	while (rec < rx_buf + rx_len) {
 		__u16 record_payload_len;
@@ -2935,7 +3237,7 @@ static size_t parse_tls_records(struct __test_metadata *_metadata,
 
 		/* Plaintext must not exceed the specified limit */
 		ASSERT_LE(plaintext_len, max_payload_len);
-		rec += rec_header_len + record_payload_len;
+		rec += TLS_HDR_LEN + record_payload_len;
 	}
 
 	return total_plaintext_rx;

3/2 2026/07/27 12:02 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 8787
							output: 36
							thoughts: 362
						

Thoughts:
**Analyzing the TLS Empty Data Record Handling Patch**

Okay, so I'm looking at a patch that tweaks the TLS implementation, specifically in `net/tls/tls_sw.c`. This is focused on how the system handles those pesky zero-length `application_data` records, the ones with no actual payload, in both TLS 1.2 and 1.3.

The key additions are pretty straightforward: they've added `tls_rx_empty_data_rec` to check for these empty records. And, critically, the code in `tls_sw_recvmsg`, `tls_sw_splice_read`, and `tls_sw_read_sock` has been modified to handle them correctly. It looks like they've had to add a check for `ctx->async_wait.err` in `tls_sw_splice_read` to account for potential crypto failures, which makes sense, as an empty record could be used to exploit an error. Plus, `tls_rx_intr_errno` has been thrown in there for signal interruption handling. The changes to `tls_sw_read_sock` look like they're bounding the time consecutive empty records can hold the socket lock, which is a good move to prevent potential denial-of-service or lock contention.

This feels like a functional change in the heart of the networking stack, the TLS implementation itself. From a security perspective, this is definitely something that needs thorough fuzzing.

My primary focus here will be to examine the functions affected. So:

*   `tls_sw_recvmsg`
*   `tls_sw_splice_read`
*   `tls_sw_read_sock`

Now, let's see how these are *used*. Are these static functions, or are they exported or used as callbacks? A quick check shows that each of these functions are used in `tls_sw.c` - a good start, but I'll need to look at how these functions are used in other files.



4/2 2026/07/27 12:02 tool grepper
0m Args:
{
  "Expression": "tls_sw_recvmsg|tls_sw_splice_read|tls_sw_read_sock"
}

Results:
{
  "Output": "net/tls/tls.h=162=void tls_sw_free_ctx_rx(struct tls_context *tls_ctx);\nnet/tls/tls.h:163:int tls_sw_recvmsg(struct sock *sk, struct msghdr *msg, size_t len,\nnet/tls/tls.h-164-\t\t   int flags);\nnet/tls/tls.h=165=bool tls_sw_sock_is_readable(struct sock *sk);\nnet/tls/tls.h:166:ssize_t tls_sw_splice_read(struct socket *sock, loff_t *ppos,\nnet/tls/tls.h-167-\t\t\t   struct pipe_inode_info *pipe,\nnet/tls/tls.h-168-\t\t\t   size_t len, unsigned int flags);\nnet/tls/tls.h:169:int tls_sw_read_sock(struct sock *sk, read_descriptor_t *desc,\nnet/tls/tls.h-170-\t\t     sk_read_actor_t read_actor);\n--\nnet/tls/tls_main.c=940=static void build_proto_ops(struct proto_ops ops[TLS_NUM_CONFIG][TLS_NUM_CONFIG],\n--\nnet/tls/tls_main.c-948-\tops[TLS_BASE][TLS_SW  ] = ops[TLS_BASE][TLS_BASE];\nnet/tls/tls_main.c:949:\tops[TLS_BASE][TLS_SW  ].splice_read\t= tls_sw_splice_read;\nnet/tls/tls_main.c-950-\tops[TLS_BASE][TLS_SW  ].poll\t\t= tls_sk_poll;\nnet/tls/tls_main.c:951:\tops[TLS_BASE][TLS_SW  ].read_sock\t= tls_sw_read_sock;\nnet/tls/tls_main.c-952-\nnet/tls/tls_main.c-953-\tops[TLS_SW  ][TLS_SW  ] = ops[TLS_SW  ][TLS_BASE];\nnet/tls/tls_main.c:954:\tops[TLS_SW  ][TLS_SW  ].splice_read\t= tls_sw_splice_read;\nnet/tls/tls_main.c-955-\tops[TLS_SW  ][TLS_SW  ].poll\t\t= tls_sk_poll;\nnet/tls/tls_main.c:956:\tops[TLS_SW  ][TLS_SW  ].read_sock\t= tls_sw_read_sock;\nnet/tls/tls_main.c-957-\n--\nnet/tls/tls_main.c=1002=static void build_protos(struct proto prot[TLS_NUM_CONFIG][TLS_NUM_CONFIG],\n--\nnet/tls/tls_main.c-1015-\tprot[TLS_BASE][TLS_SW] = prot[TLS_BASE][TLS_BASE];\nnet/tls/tls_main.c:1016:\tprot[TLS_BASE][TLS_SW].recvmsg\t\t  = tls_sw_recvmsg;\nnet/tls/tls_main.c-1017-\tprot[TLS_BASE][TLS_SW].sock_is_readable   = tls_sw_sock_is_readable;\n--\nnet/tls/tls_main.c-1020-\tprot[TLS_SW][TLS_SW] = prot[TLS_SW][TLS_BASE];\nnet/tls/tls_main.c:1021:\tprot[TLS_SW][TLS_SW].recvmsg\t\t= tls_sw_recvmsg;\nnet/tls/tls_main.c-1022-\tprot[TLS_SW][TLS_SW].sock_is_readable   = tls_sw_sock_is_readable;\n--\nnet/tls/tls_sw.c=1804=static int tls_rx_intr_errno(long timeo)\n--\nnet/tls/tls_sw.c-1808-\nnet/tls/tls_sw.c:1809:int tls_sw_recvmsg(struct sock *sk,\nnet/tls/tls_sw.c-1810-\t\t   struct msghdr *msg,\n--\nnet/tls/tls_sw.c-2025-\nnet/tls/tls_sw.c:2026:ssize_t tls_sw_splice_read(struct socket *sock,  loff_t *ppos,\nnet/tls/tls_sw.c-2027-\t\t\t   struct pipe_inode_info *pipe,\n--\nnet/tls/tls_sw.c-2129-\nnet/tls/tls_sw.c:2130:int tls_sw_read_sock(struct sock *sk, read_descriptor_t *desc,\nnet/tls/tls_sw.c-2131-\t\t     sk_read_actor_t read_actor)\n--\ntools/testing/selftests/net/tls.c=2914=TEST_F(tls_err, bad_auth)\n--\ntools/testing/selftests/net/tls.c-2934- * and fails on the record itself; the ctx-\u003easync_wait.err check in\ntools/testing/selftests/net/tls.c:2935: * tls_sw_splice_read() is not what stops it. Only an asynchronous\ntools/testing/selftests/net/tls.c-2936- * decrypt, which needs a TLS 1.2 socket and an AEAD advertising\n"
}

5/2 2026/07/27 12:02 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 10986
							output: 16
							thoughts: 78
						

Thoughts:
Okay, here's my take on those functions and their significance, as though I'm processing the information:

**Key TLS Socket Function Focus**

Alright, looking at this code, it's clear that `tls_sw_recvmsg`, `tls_sw_splice_read`, and `tls_sw_read_sock` are the bread and butter functions to focus on for TLS socket I/O. They're definitely the key symbols to understand how data moves within the TLS layer. The fact that they're tied to the `proto` and `proto_ops` structures is crucial. It means these functions are directly integrated into the standard socket operations – the `recvmsg`, `splice`, the general read paths. It means they will be triggered when standard socket calls are made, which is exactly where I'd expect to see the TLS interaction. Now I need to check the relevant configuration flags to get a complete picture. What are the build requirements to bring this TLS functionality to life? What configurations are needed for the system to support this functionality?



6/2 2026/07/27 12:02 tool grepper
0m Args:
{
  "Expression": "config TLS"
}

Results:
{
  "Output": "arch/arm/mm/Kconfig=873=config HARDEN_BRANCH_HISTORY\n--\narch/arm/mm/Kconfig-882-\narch/arm/mm/Kconfig:883:config TLS_REG_EMUL\narch/arm/mm/Kconfig-884-\tbool\n--\nnet/tls/Kconfig-4-#\nnet/tls/Kconfig:5:config TLS\nnet/tls/Kconfig-6-\ttristate \"Transport Layer Security support\"\n--\nnet/tls/Kconfig-18-\nnet/tls/Kconfig:19:config TLS_DEVICE\nnet/tls/Kconfig-20-\tbool \"Transport Layer Security HW offload\"\n"
}

7/2 2026/07/27 12:02 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 11479
							output: 32
							thoughts: 70
						

Thoughts:
**TLS Configuration Assessment**

Okay, so the configuration in question is `TLS`. My initial thought is to confirm that this is the *only* necessary configuration. Let's see... the patch is modifying `net/tls/tls_sw.c`. Ah, right, that's firmly within the TLS subsystem. Given that context, `CONFIG_TLS` alone *should* be sufficient. However, I always make sure. I'll need to double check that there aren't any hidden dependencies that aren't immediately obvious. It's a good practice, even when it seems straightforward, to be thorough.



8/2 2026/07/27 12:02 tool read-file
0m Args:
{
  "File": "net/tls/Kconfig",
  "FirstLine": 1,
  "LineCount": 20
}

Results:
{
  "Contents": "   1:\t# SPDX-License-Identifier: GPL-2.0-only\n   2:\t#\n   3:\t# TLS configuration\n   4:\t#\n   5:\tconfig TLS\n   6:\t\ttristate \"Transport Layer Security support\"\n   7:\t\tdepends on INET\n   8:\t\tselect CRYPTO\n   9:\t\tselect CRYPTO_AES\n  10:\t\tselect CRYPTO_GCM\n  11:\t\tselect NET_SOCK_MSG\n  12:\t\tdefault n\n  13:\t\thelp\n  14:\t\tEnable kernel support for TLS protocol. This allows symmetric\n  15:\t\tencryption handling of the TLS protocol to be done in-kernel.\n  16:\t\n  17:\t\tIf unsure, say N.\n  18:\t\n  19:\tconfig TLS_DEVICE\n  20:\t\tbool \"Transport Layer Security HW offload\"\n"
}

9/2 2026/07/27 12:02 llm patch-evaluator
0m Model:
gemini-3.1-pro-preview

Args:
null

Results:
null

Tokens:
							input: 11933
							output: 128
							thoughts: 377
						

Thoughts:
**My Analysis of this Patch**

Okay, so I'm looking at this patch and immediately, I see the dependency: `CONFIG_TLS` relies on `INET`.  `INET` is practically always enabled, which means, essentially, I just need to explicitly enable `TLS`.

The patch also brings up `TLS 1.2` and `TLS 1.3`.  But, as far as I can tell, there aren't any separate config options specifically for those; it's all handled under the umbrella of `CONFIG_TLS`. Good to know.

Next, I need to understand the scope of the code changes. I see these modified functions: `tls_rx_empty_data_rec` (static), `tls_rx_intr_errno` (static), `tls_sw_recvmsg`, `tls_sw_splice_read`, and `tls_sw_read_sock`. The static ones, I can generally ignore for now.

Right, so the important symbols here are `tls_sw_recvmsg`, `tls_sw_splice_read`, and `tls_sw_read_sock`.

Alright, it's about handling empty data records in TLS 1.2 and 1.3. This is a clear functional change to how TLS receives data. It's in the receive path, and these socket operations, like `recvmsg`, are directly exposed to userspace.

That tells me I need to mark `WorthFuzzing` as `true`.  This is a critical area for fuzzing.

Now, let's just make sure the `set-results` parameters are configured correctly:

*   `EnableConfigs`: `["TLS"]` – Check.
*   `FocusSymbols`: `["tls_sw_recvmsg", "tls_sw_splice_read", "tls_sw_read_sock"]` – Check.
*   `Reasoning`: "The patch modifies the TLS software receive path to properly handle zero-length application_data records (empty data records) in TLS 1.2 and TLS 1.3. These changes affect `recvmsg`, `splice_read`, and `read_sock` operations on TLS sockets, which are reachable from userspace." – Perfect, covers the core issue.
*   `WorthFuzzing`: `true` – That's the correct evaluation.

Looks good. Time to call `set-results`.



10/2 2026/07/27 12:03 tool set-results
0m Args:
{
  "EnableConfigs": [
    "TLS"
  ],
  "FocusSymbols": [
    "tls_sw_recvmsg",
    "tls_sw_splice_read",
    "tls_sw_read_sock"
  ],
  "Reasoning": "The patch modifies the TLS software receive path to properly handle zero-length application_data records (empty data records) in TLS 1.2 and TLS 1.3. These changes affect `recvmsg`, `splice_read`, and `read_sock` operations on TLS sockets, which are reachable from userspace.",
  "WorthFuzzing": true
}

Results:
{
  "EnableConfigs": [
    "TLS"
  ],
  "FocusSymbols": [
    "tls_sw_recvmsg",
    "tls_sw_splice_read",
    "tls_sw_read_sock"
  ],
  "Reasoning": "The patch modifies the TLS software receive path to properly handle zero-length application_data records (empty data records) in TLS 1.2 and TLS 1.3. These changes affect `recvmsg`, `splice_read`, and `read_sock` operations on TLS sockets, which are reachable from userspace.",
  "WorthFuzzing": true
}

LLM Calls Summary:
Total Calls Total Tokens Avg Tokens Total Duration (Seconds) Avg Duration (Seconds)
Tool Calls Summary:
Total Calls Total Duration (Seconds) Avg Duration (Seconds)