From: Aohan Mei <ljp1205831794@gmail.com>
To: netdev@vger.kernel.org
Cc: linux-sctp@vger.kernel.org, marcelo.leitner@gmail.com,
lucien.xin@gmail.com, imv4bel@gmail.com,
Aohan Mei <henrymei@tencent.com>,
TencentOS Corvus AI <corvus@tencent.com>,
stable@vger.kernel.org
Subject: [PATCH net] sctp: don't re-register a removed transport as last_data_from
Date: Wed, 16 Sep 2026 16:09:51 +0800 [thread overview]
Message-ID: <20260916080955.1019050-1-ljp1205831794@gmail.com> (raw)
From: Aohan Mei <henrymei@tencent.com>
When an association is in COOKIE-ECHOED state and the peer sends a
bundled [ERROR(Stale Cookie)][DATA] packet from one of its non-primary
addresses, processing the ERROR chunk takes the non-fatal stale-cookie
retry path sctp_sf_do_5_2_6_stale(), which queues
SCTP_CMD_DEL_NON_PRIMARY while keeping the association alive.
sctp_cmd_del_non_primary() removes every non-primary transport -
including the very transport this packet arrived on, which is still
referenced by the receive lookup and shared by all chunks of the
packet via chunk->transport.
sctp_assoc_rm_peer() does redirect asoc->peer.last_data_from away from
the removed transport, but right afterwards the bundled DATA chunk
makes sctp_assoc_bh_rcv() re-register
asoc->peer.last_data_from = chunk->transport unconditionally, undoing
the redirection with the just-removed transport.
Once the packet is done, the receive reference is dropped and the
transport is RCU-freed, while the surviving association keeps the
dangling last_data_from. A later FWD-TSN (or the delayed SACK timer)
makes sctp_gen_sack() dereference it (->param_flags and friends), and
sctp_make_sack()/sctp_outq_select_transport() may write to the freed
object and link it into the live transport list. This is a
use-after-free triggerable by any malicious SCTP peer (or a local
unprivileged user acting as one) with no capabilities required:
BUG: KASAN: slab-use-after-free in sctp_do_sm+0x498a/0x5660
Read of size 4 at addr ffff88800e1e356c by task poc/115
Call Trace: sctp_do_sm <- sctp_assoc_bh_rcv <- sctp_inq_push <-
sctp_rcv <- ip_protocol_deliver_rcu <- ip_rcv
Allocated: sctp_transport_new <- sctp_assoc_add_peer <-
sctp_process_init (INIT-ACK processing)
Freed: kfree <- sctp_transport_destroy_rcu <- rcu_core
(call_rcu queued by sctp_transport_put at end of sctp_rcv)
The buggy address is located 364 bytes inside of freed 1024-byte
region [ffff88800e1e3400, ffff88800e1e3800), cache kmalloc-1k
Related is commit 03a9d10ecf71 ("sctp: drop a chunk if its transport was
removed"), which only covers the window between the receive lookup and
the chunk processing (e.g. an ASCONF DEL-IP racing the socket backlog);
here the transport is removed *while* the packet is being processed, by
an earlier chunk of the same packet, so the drop in sctp_inq_push() does
not reach this path. Verified with the bundled [ERROR(Stale
Cookie)][DATA] + FWD-TSN reproducer: the KASAN report above still fires
with that commit applied, and is gone with this patch on top.
Fix it by never registering a dead transport as last_data_from:
sctp_transport_free() sets ->dead when the transport is removed, so
both re-registration sites (the association and the endpoint backlog
paths) can simply skip it, keeping the redirection done by
sctp_assoc_rm_peer() in effect.
Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
Reported-by: TencentOS Corvus AI <corvus@tencent.com>
Cc: stable@vger.kernel.org
Assisted-by: CodeBuddy:Kimi-K3
Signed-off-by: Aohan Mei <henrymei@tencent.com>
---
net/sctp/associola.c | 16 ++++++++++++----
net/sctp/endpointola.c | 10 ++++++----
2 files changed, 18 insertions(+), 8 deletions(-)
diff --git a/net/sctp/associola.c b/net/sctp/associola.c
index 5b0ae616e1ff9..ecef09959630d 100644
--- a/net/sctp/associola.c
+++ b/net/sctp/associola.c
@@ -1023,11 +1023,19 @@ static void sctp_assoc_bh_rcv(struct work_struct *work)
continue;
/* Remember where the last DATA chunk came from so we
- * know where to send the SACK.
+ * know where to send the SACK. chunk->transport may have
+ * been removed while processing an earlier chunk of this
+ * same packet (e.g. a stale-cookie ERROR chunk queues
+ * SCTP_CMD_DEL_NON_PRIMARY, which removes the non-primary
+ * transport this packet arrived on), so never register a
+ * dead transport; otherwise last_data_from would be left
+ * dangling once the receive reference is dropped and the
+ * transport is freed.
*/
- if (sctp_chunk_is_data(chunk))
- asoc->peer.last_data_from = chunk->transport;
- else {
+ if (sctp_chunk_is_data(chunk)) {
+ if (!chunk->transport || !chunk->transport->dead)
+ asoc->peer.last_data_from = chunk->transport;
+ } else {
SCTP_INC_STATS(net, SCTP_MIB_INCTRLCHUNKS);
asoc->stats.ictrlchunks++;
if (chunk->chunk_hdr->type == SCTP_CID_SACK)
diff --git a/net/sctp/endpointola.c b/net/sctp/endpointola.c
index dfb1719275dba..f9d4f318e132e 100644
--- a/net/sctp/endpointola.c
+++ b/net/sctp/endpointola.c
@@ -392,11 +392,13 @@ static void sctp_endpoint_bh_rcv(struct work_struct *work)
continue;
/* Remember where the last DATA chunk came from so we
- * know where to send the SACK.
+ * know where to send the SACK. As in sctp_assoc_bh_rcv(),
+ * never register a dead (already removed) transport.
*/
- if (asoc && sctp_chunk_is_data(chunk))
- asoc->peer.last_data_from = chunk->transport;
- else {
+ if (asoc && sctp_chunk_is_data(chunk)) {
+ if (!chunk->transport || !chunk->transport->dead)
+ asoc->peer.last_data_from = chunk->transport;
+ } else {
SCTP_INC_STATS(ep->base.net, SCTP_MIB_INCTRLCHUNKS);
if (asoc)
asoc->stats.ictrlchunks++;
--
2.43.7
next reply other threads:[~2026-09-16 8:10 UTC|newest]
Thread overview: 7+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-16 8:09 Aohan Mei [this message]
2026-09-17 23:10 ` [PATCH net] sctp: don't re-register a removed transport as last_data_from netdev-bot+sashiko
2026-09-18 19:16 ` Xin Long
2026-09-21 9:37 ` [PATCH net v2] sctp: discard the rest of the packet on a stale-cookie error Aohan Mei
2026-09-22 15:21 ` Xin Long
2026-09-22 21:57 ` Xin Long
2026-09-23 1:40 ` patchwork-bot+netdevbpf
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260916080955.1019050-1-ljp1205831794@gmail.com \
--to=ljp1205831794@gmail.com \
--cc=corvus@tencent.com \
--cc=henrymei@tencent.com \
--cc=imv4bel@gmail.com \
--cc=linux-sctp@vger.kernel.org \
--cc=lucien.xin@gmail.com \
--cc=marcelo.leitner@gmail.com \
--cc=netdev@vger.kernel.org \
--cc=stable@vger.kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.