* [PATCH net 1/2] tls: don't over-fill the plaintext sk_msg ring in tls_sw_sendmsg_splice()
2026-07-26 10:55 [PATCH net 0/2] tls: fix plaintext sk_msg ring over-fill in tls_sw_sendmsg_splice() chanyoung
@ 2026-07-26 10:55 ` chanyoung
2026-07-26 10:55 ` [PATCH net 2/2] selftests: tls: add a test for splicing onto a full plaintext record chanyoung
1 sibling, 0 replies; 3+ messages in thread
From: chanyoung @ 2026-07-26 10:55 UTC (permalink / raw)
To: netdev
Cc: John Fastabend, Jakub Kicinski, Sabrina Dubroca, David Howells,
Shuah Khan, linux-kselftest, chanyoung, stable
tls_sw_sendmsg_splice() appends pages to the open record's plaintext
sk_msg ring with sk_msg_page_add(), which performs no fullness check of
its own, and the loop only tests sk_msg_full() at the bottom of its
do-while.
If the ring is already full when the function is entered, the first
sk_msg_page_add() writes the reserved slot and sk_msg_iter_next() wraps
sg.end around to sg.start. sk_msg_iter_dist() then returns 0, so
sk_msg_full() reports the ring as empty, the loop keeps running, and each
further add overwrites a live entry without putting its page reference
while sg.size keeps growing. sg.size is then larger than the data
reachable by walking the logical [sg.start, sg.end) ring.
tls_push_record() marks the end of the scatterlist at the logical last
entry but passes the inflated msg_pl->sg.size to tls_do_encryption() as
cryptlen, so the AEAD scatterwalk runs past the end-marked entry and
dereferences the NULL returned by sg_next():
BUG: kernel NULL pointer dereference, address: 0000000000000008
CPU: 1 UID: 1000 PID: 204 Comm: exploit Not tainted 7.2.0-rc4+ #1 PREEMPTLAZY
RIP: 0010:memcpy_from_scatterwalk+0x32/0xc0
Call Trace:
<TASK>
skcipher_walk_next+0x1d1/0x2c0
gcm_encrypt_aesni_avx+0x1e9/0x220
bpf_exec_tx_verdict+0x3bb/0x860
tls_sw_sendmsg+0xa1a/0xca0
__sys_sendto+0x1da/0x1f0
do_syscall_64+0xdc/0x520
entry_SYSCALL_64_after_hwframe+0x76/0x7e
</TASK>
An unprivileged user can reach this on a plain loopback TCP socket with
the "tls" ULP attached. A full but unpushed plaintext ring survives
across a syscall through the copy path: sk_msg_clone() returns 0 rather
than -ENOSPC for the frag that makes the ring exactly full, because its
guard is "if (i == src->sg.end && len)" and len reaches 0 as that frag is
added, so full_record is never set and MSG_MORE keeps eor clear. Since
record_room is a byte count, a frag-exhausted ring that holds only a few
hundred bytes still admits the next splice(), which then re-enters
tls_sw_sendmsg_splice() on a full ring.
The caller already handles a ring that becomes full during the splice by
testing sk_msg_full() afterwards and setting full_record to push the
record, so the loop condition only needs to be evaluated before the first
sk_msg_page_add() rather than after it. Turn the do-while into a while
loop: when the ring is full on entry the function returns without adding
anything, the caller pushes the record, and the next iteration of the
caller's loop starts from a fresh, empty ring.
Cc: stable@vger.kernel.org
Fixes: fe1e81d4f73b ("tls/sw: Support MSG_SPLICE_PAGES")
Signed-off-by: Chanyoung Park <ppoo1220@gmail.com>
---
net/tls/tls_sw.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/net/tls/tls_sw.c b/net/tls/tls_sw.c
index d4afc90fd79..0c413d05bb1 100644
--- a/net/tls/tls_sw.c
+++ b/net/tls/tls_sw.c
@@ -738,7 +738,7 @@ static int tls_sw_sendmsg_splice(struct sock *sk, struct msghdr *msg,
{
struct page *page = NULL, **pages = &page;
- do {
+ while (try_to_copy && !sk_msg_full(msg_pl)) {
ssize_t part;
size_t off;
@@ -758,7 +758,7 @@ static int tls_sw_sendmsg_splice(struct sock *sk, struct msghdr *msg,
sk_mem_charge(sk, part);
*copied += part;
try_to_copy -= part;
- } while (try_to_copy && !sk_msg_full(msg_pl));
+ }
return 0;
}
--
2.43.0
^ permalink raw reply related [flat|nested] 3+ messages in thread* [PATCH net 2/2] selftests: tls: add a test for splicing onto a full plaintext record
2026-07-26 10:55 [PATCH net 0/2] tls: fix plaintext sk_msg ring over-fill in tls_sw_sendmsg_splice() chanyoung
2026-07-26 10:55 ` [PATCH net 1/2] tls: don't over-fill the plaintext sk_msg ring " chanyoung
@ 2026-07-26 10:55 ` chanyoung
1 sibling, 0 replies; 3+ messages in thread
From: chanyoung @ 2026-07-26 10:55 UTC (permalink / raw)
To: netdev
Cc: John Fastabend, Jakub Kicinski, Sabrina Dubroca, David Howells,
Shuah Khan, linux-kselftest, chanyoung
Splicing into an open record whose plaintext scatterlist ring was already
full used to wrap the ring's end index onto its start. sk_msg_full() then
reported the full ring as empty, so the loop kept overwriting live entries
while sg.size grew, and pushing the record ran the AEAD scatterwalk off the
end of the scatterlist.
Reaching that state needs the ring to be left full and unpushed across a
syscall, which the copy path does when the fragment it adds is the one that
fills the ring. The test therefore fills the ring with splices, adds one
byte with MSG_MORE, splices some more, and only then pushes the record.
MAX_SKB_FRAGS is configurable, so rather than hardcoding the number of
fragments needed to fill the ring, sweep it over the plausible range so the
one-byte send lands exactly on a full ring whatever the kernel was built
with.
On an unpatched kernel this oopses in the AEAD walk; with the preceding
patch applied the whole tls selftest suite passes.
Signed-off-by: Chanyoung Park <ppoo1220@gmail.com>
---
tools/testing/selftests/net/tls.c | 52 +++++++++++++++++++++++++++++++
1 file changed, 52 insertions(+)
diff --git a/tools/testing/selftests/net/tls.c b/tools/testing/selftests/net/tls.c
index cbdd3ea28b9..d2666884ea8 100644
--- a/tools/testing/selftests/net/tls.c
+++ b/tools/testing/selftests/net/tls.c
@@ -835,6 +835,58 @@ TEST_F(tls, send_and_splice)
EXPECT_EQ(memcmp(mem_send, mem_recv, send_len), 0);
}
+/* Splicing into an open record whose plaintext scatterlist ring is already
+ * full used to wrap the ring's end index onto its start, after which
+ * sk_msg_full() reported the full ring as empty: further pages overwrote
+ * live entries and sg.size desynced from the walkable scatterlist, which
+ * oopsed in the AEAD walk once the record was pushed. The ring is left
+ * full and unpushed by the copy path, which does not push the record when
+ * the fragment it adds is the one that fills the ring.
+ */
+TEST_F(tls, splice_onto_full_record)
+{
+ int frag_len = 100, extra = 4;
+ char mem_send[5000];
+ char mem_recv[5000];
+ int nfrags, i, total;
+ int p[2];
+
+ memrnd(mem_send, sizeof(mem_send));
+
+ /* MAX_SKB_FRAGS is configurable (17 by default), so sweep the
+ * plausible range to land the one-byte send exactly on a full ring
+ * whatever this kernel was built with.
+ */
+ for (nfrags = 12; nfrags <= 45; nfrags++) {
+ total = (nfrags + extra) * frag_len + 2;
+
+ for (i = 0; i < nfrags; i++) {
+ ASSERT_GE(pipe(p), 0);
+ EXPECT_EQ(write(p[1], mem_send, frag_len), frag_len);
+ EXPECT_EQ(splice(p[0], NULL, self->fd, NULL, frag_len,
+ SPLICE_F_MORE), frag_len);
+ close(p[0]);
+ close(p[1]);
+ }
+
+ EXPECT_EQ(send(self->fd, mem_send, 1, MSG_MORE), 1);
+
+ for (i = 0; i < extra; i++) {
+ ASSERT_GE(pipe(p), 0);
+ EXPECT_EQ(write(p[1], mem_send, frag_len), frag_len);
+ EXPECT_EQ(splice(p[0], NULL, self->fd, NULL, frag_len,
+ SPLICE_F_MORE), frag_len);
+ close(p[0]);
+ close(p[1]);
+ }
+
+ EXPECT_EQ(send(self->fd, mem_send, 1, 0), 1);
+
+ EXPECT_EQ(recv(self->cfd, mem_recv, total, MSG_WAITALL), total);
+ EXPECT_EQ(memcmp(mem_send, mem_recv, frag_len), 0);
+ }
+}
+
TEST_F(tls, splice_to_pipe)
{
int send_len = TLS_PAYLOAD_MAX_LEN;
--
2.43.0
^ permalink raw reply related [flat|nested] 3+ messages in thread