[PATCH net v2] sctp: revalidate output stream after association connect wait
From: Daehyeon Ko
Date: Tue Oct 06 2026 - 23:58:38 EST
When message interleaving is enabled, the first send waits for association
establishment before building its data chunks. The wait drops the socket
lock, and handshake processing can reduce the output stream count to the
peer-advertised inbound stream count. sctp_stream_init() then frees the
extension of every removed stream.
The sender resumes with the stream that it checked before the wait. If the
peer removed that stream, sctp_outq_tail() later dereferences its NULL
extension. Fatal-oops policies then panic the host.
KASAN: null-ptr-deref in range [0x38-0x3f]
RIP: sctp_outq_tail+0x49e/0xaa0
Call Trace:
sctp_primitive_SEND
sctp_sendmsg_to_asoc
sctp_sendmsg
A range check alone is insufficient. Stream reconfiguration can grow the
output count again while the lock is released without recreating extensions
freed by the earlier shrink. The stream can then be in range while its
extension remains NULL. The send-buffer wait has the same issue.
Factor the range and extension checks into a helper and repeat both after
each send-path wait that drops the socket lock. Once the lock is
reacquired, the validation remains stable through data creation and
queueing.
If validation fails after the connect wait, the auto-created association is
already established. Return ESRCH so the existing caller path keeps it
under state-machine ownership instead of freeing it directly, which would
leave protocol, counter and socket state inconsistent.
Fixes: 668c9beb9020 ("sctp: implement assign_number for sctp_stream_interleave")
Cc: stable@xxxxxxxxxxxxxxx
Assisted-by: LLM
Signed-off-by: Daehyeon Ko <4ncienth@xxxxxxxxx>
---
Changes in v2:
- Repeat full stream validation after both send-path waits.
- Recreate a missing stream extension after shrink followed by growth.
- Return ESRCH after the connect wait to retain the live association.
v1: https://lore.kernel.org/r/20261002010449.3689454-1-4ncienth@xxxxxxxxx
net/sctp/socket.c | 33 +++++++++++++++++++++------------
1 file changed, 21 insertions(+), 12 deletions(-)
diff --git a/net/sctp/socket.c b/net/sctp/socket.c
index 4652fd90d9a6c4..aaeb58ce055771 100644
--- a/net/sctp/socket.c
+++ b/net/sctp/socket.c
@@ -1786,6 +1786,18 @@ static int sctp_sendmsg_check_sflags(struct sctp_association *asoc,
return 1;
}
+static int sctp_sendmsg_check_stream(struct sctp_association *asoc,
+ struct sctp_sndrcvinfo *sinfo)
+{
+ if (unlikely(sinfo->sinfo_stream >= asoc->stream.outcnt))
+ return -EINVAL;
+
+ if (unlikely(!SCTP_SO(&asoc->stream, sinfo->sinfo_stream)->ext))
+ return sctp_stream_init_ext(&asoc->stream, sinfo->sinfo_stream);
+
+ return 0;
+}
+
static int sctp_sendmsg_to_asoc(struct sctp_association *asoc,
struct msghdr *msg, size_t msg_len,
struct sctp_transport *transport,
@@ -1800,16 +1812,9 @@ static int sctp_sendmsg_to_asoc(struct sctp_association *asoc,
long timeo;
int err;
- if (sinfo->sinfo_stream >= asoc->stream.outcnt) {
- err = -EINVAL;
+ err = sctp_sendmsg_check_stream(asoc, sinfo);
+ if (err)
goto err;
- }
-
- if (unlikely(!SCTP_SO(&asoc->stream, sinfo->sinfo_stream)->ext)) {
- err = sctp_stream_init_ext(&asoc->stream, sinfo->sinfo_stream);
- if (err)
- goto err;
- }
if (sp->disable_fragments && msg_len > asoc->frag_point) {
err = -EMSGSIZE;
@@ -1830,10 +1835,9 @@ static int sctp_sendmsg_to_asoc(struct sctp_association *asoc,
err = sctp_wait_for_sndbuf(asoc, transport, &timeo, msg_len);
if (err)
goto err;
- if (unlikely(sinfo->sinfo_stream >= asoc->stream.outcnt)) {
- err = -EINVAL;
+ err = sctp_sendmsg_check_stream(asoc, sinfo);
+ if (err)
goto err;
- }
}
if (sctp_state(asoc, CLOSED)) {
@@ -1848,6 +1852,11 @@ static int sctp_sendmsg_to_asoc(struct sctp_association *asoc,
err = -ESRCH;
goto err;
}
+ err = sctp_sendmsg_check_stream(asoc, sinfo);
+ if (err) {
+ err = -ESRCH;
+ goto err;
+ }
} else {
wait_connect = true;
}
--
2.55.0