On Wed, Apr 02, 2025 at 12:25:36PM +0200, Ricardo Cañuelo Navarro wrote:
sctp_sendmsg() re-uses associations and transports when possible by doing a lookup based on the socket endpoint and the message destination address, and then sctp_sendmsg_to_asoc() sets the selected transport in all the message chunks to be sent.
There's a possible race condition if another thread triggers the removal of that selected transport, for instance, by explicitly unbinding an address with setsockopt(SCTP_SOCKOPT_BINDX_REM), after the chunks have been set up and before the message is sent. This causes the access to the transport data in sctp_outq_select_transport(), when the association outqueue is flushed, to do a use-after-free read.
This patch addresses this scenario by checking if the transport still exists right after the chunks to be sent are set up to use it and before proceeding to sending them. If the transport was freed since it was found, the send is aborted. The reason to add the check here is that once the transport is assigned to the chunks, deleting that transport is safe, since it will also set chunk->transport to NULL in the affected chunks. This scenario is correctly handled already, see Fixes below.
The bug was found by a private syzbot instance (see the error report [1] and the C reproducer that triggers it [2]).
Link: https://people.igalia.com/rcn/kernel_logs/20250402__KASAN_slab-use-after-fre... [1] Link: https://people.igalia.com/rcn/kernel_logs/20250402__KASAN_slab-use-after-fre... [2] Cc: stable@vger.kernel.org Fixes: df132eff4638 ("sctp: clear the transport of some out_chunk_list chunks in sctp_assoc_rm_peer") Signed-off-by: Ricardo Cañuelo Navarro rcn@igalia.com
net/sctp/socket.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+)
diff --git a/net/sctp/socket.c b/net/sctp/socket.c index 36ee34f483d703ffcfe5ca9e6cc554fba24c75ef..9c5ff44fa73cae6a6a04790800cc33dfa08a8da9 100644 --- a/net/sctp/socket.c +++ b/net/sctp/socket.c @@ -1787,17 +1787,24 @@ static int sctp_sendmsg_check_sflags(struct sctp_association *asoc, return 1; } +static union sctp_addr *sctp_sendmsg_get_daddr(struct sock *sk,
const struct msghdr *msg,
struct sctp_cmsgs *cmsgs);
static int sctp_sendmsg_to_asoc(struct sctp_association *asoc, struct msghdr *msg, size_t msg_len, struct sctp_transport *transport, struct sctp_sndrcvinfo *sinfo) {
- struct sctp_transport *aux_transport = NULL; struct sock *sk = asoc->base.sk;
- struct sctp_endpoint *ep = sctp_sk(sk)->ep; struct sctp_sock *sp = sctp_sk(sk); struct net *net = sock_net(sk); struct sctp_datamsg *datamsg; bool wait_connect = false; struct sctp_chunk *chunk;
- union sctp_addr *daddr; long timeo; int err;
@@ -1869,6 +1876,15 @@ static int sctp_sendmsg_to_asoc(struct sctp_association *asoc, sctp_set_owner_w(chunk); chunk->transport = transport; }
- /* Fail if transport was deleted after lookup in sctp_sendmsg() */
- daddr = sctp_sendmsg_get_daddr(sk, msg, NULL);
- if (daddr) {
sctp_endpoint_lookup_assoc(ep, daddr, &aux_transport);
if (!aux_transport || aux_transport != transport) {
sctp_datamsg_free(datamsg);
goto err;
Hi Ricardo,
This is not a full review, and I would suggest waiting for one from others. But this will result in the local variable err being used uninitialised.
Flagged by Smatch.
}
- }
err = sctp_primitive_SEND(net, asoc, datamsg); if (err) {