* [PATCH nf 0/5] conntrack expectation fixes
@ 2026-03-20 12:59 Pablo Neira Ayuso
2026-03-20 12:59 ` [PATCH nf 1/5] netfilter: nf_conntrack_expect: honor expectation helper field Pablo Neira Ayuso
` (4 more replies)
0 siblings, 5 replies; 7+ messages in thread
From: Pablo Neira Ayuso @ 2026-03-20 12:59 UTC (permalink / raw)
To: netfilter-devel; +Cc: fw
Hi,
This series addresses races in the conntrack expectation
subsystem.
Patch #1 and #2 honor the exp->helper as a replacement to accessing
exp->master->helper which is unsafe when accessed away from the
nf_conntrack_expect spinlock, because the ct->ext is released
immediately, hence rcu_read_lock section cannot help in such case. On
the contrary, accessing exp->helper under rcu_read_lock section is safe.
Patch #3 extends the spinlock section in delete and get expectation
commands in ctnetlink, because holding a reference on the expectation
is not sufficient, the master conntrack can go away rendering
exp->master unsafe.
Patch #4 and #5 move the netns and zone to the expectation object.
This is to fix /proc/net/nf_conntrack_expect which is dumping the
global expectation table in every netns. By moving these fields
to the expectation, it is safe to access them under rcu_read_lock
section.
This passing tests with shell and scapy generated traffic to create
expectations for the ftp and tftp helpers, I am still reviewing the
remaining helpers in the tree. I will come back with a confirmation
that all is ok on that front too.
Pablo Neira Ayuso (5):
netfilter: nf_conntrack_expect: honor expectation helper field
netfilter: nf_conntrack_expect: use expect->helper
netfilter: ctnetlink: ensure safe access to master conntrack
netfilter: nf_conntrack_expect: store netns and zone in expectation
netfilter: nf_conntrack_expect: skip expectations in other netns via proc
include/net/netfilter/nf_conntrack_core.h | 8 +++
include/net/netfilter/nf_conntrack_expect.h | 20 ++++++-
net/netfilter/nf_conntrack_broadcast.c | 2 +-
net/netfilter/nf_conntrack_ecache.c | 2 +
net/netfilter/nf_conntrack_expect.c | 30 +++++++++--
net/netfilter/nf_conntrack_h323_main.c | 12 ++---
net/netfilter/nf_conntrack_helper.c | 8 ++-
net/netfilter/nf_conntrack_netlink.c | 58 ++++++++++++---------
net/netfilter/nf_conntrack_sip.c | 4 +-
9 files changed, 101 insertions(+), 43 deletions(-)
--
2.47.3
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH nf 1/5] netfilter: nf_conntrack_expect: honor expectation helper field
2026-03-20 12:59 [PATCH nf 0/5] conntrack expectation fixes Pablo Neira Ayuso
@ 2026-03-20 12:59 ` Pablo Neira Ayuso
2026-03-20 12:59 ` [PATCH nf 2/5] netfilter: nf_conntrack_expect: use expect->helper Pablo Neira Ayuso
` (3 subsequent siblings)
4 siblings, 0 replies; 7+ messages in thread
From: Pablo Neira Ayuso @ 2026-03-20 12:59 UTC (permalink / raw)
To: netfilter-devel; +Cc: fw
The expectation helper field is mostly unused. As a result, the
netfilter codebase relies on accessing the helper through exp->master.
Always set on the expectation helper field so it can be used to reach
the helper.
This is a preparation patches for follow up fixes.
Signed-off-by: Pablo Neira Ayuso <pablo@netfilter.org>
---
include/net/netfilter/nf_conntrack_expect.h | 2 +-
net/netfilter/nf_conntrack_broadcast.c | 2 +-
net/netfilter/nf_conntrack_expect.c | 6 +++++-
net/netfilter/nf_conntrack_h323_main.c | 12 ++++++------
net/netfilter/nf_conntrack_helper.c | 5 +++++
net/netfilter/nf_conntrack_netlink.c | 2 +-
net/netfilter/nf_conntrack_sip.c | 2 +-
7 files changed, 20 insertions(+), 11 deletions(-)
diff --git a/include/net/netfilter/nf_conntrack_expect.h b/include/net/netfilter/nf_conntrack_expect.h
index 165e7a03b8e9..1b01400b10bd 100644
--- a/include/net/netfilter/nf_conntrack_expect.h
+++ b/include/net/netfilter/nf_conntrack_expect.h
@@ -40,7 +40,7 @@ struct nf_conntrack_expect {
struct nf_conntrack_expect *this);
/* Helper to assign to new connection */
- struct nf_conntrack_helper *helper;
+ struct nf_conntrack_helper __rcu *helper;
/* The conntrack of the master connection */
struct nf_conn *master;
diff --git a/net/netfilter/nf_conntrack_broadcast.c b/net/netfilter/nf_conntrack_broadcast.c
index a7552a46d6ac..d21576fcb1d6 100644
--- a/net/netfilter/nf_conntrack_broadcast.c
+++ b/net/netfilter/nf_conntrack_broadcast.c
@@ -70,7 +70,7 @@ int nf_conntrack_broadcast_help(struct sk_buff *skb,
exp->expectfn = NULL;
exp->flags = NF_CT_EXPECT_PERMANENT;
exp->class = NF_CT_EXPECT_CLASS_DEFAULT;
- exp->helper = NULL;
+ rcu_assign_pointer(exp->helper, nfct_help(ct)->helper);
nf_ct_expect_related(exp, 0);
nf_ct_expect_put(exp);
diff --git a/net/netfilter/nf_conntrack_expect.c b/net/netfilter/nf_conntrack_expect.c
index cfc2daa3fc7f..197a76d0b231 100644
--- a/net/netfilter/nf_conntrack_expect.c
+++ b/net/netfilter/nf_conntrack_expect.c
@@ -309,6 +309,10 @@ struct nf_conntrack_expect *nf_ct_expect_alloc(struct nf_conn *me)
}
EXPORT_SYMBOL_GPL(nf_ct_expect_alloc);
+/* This function can only be used from packet path, where accessing
+ * master's helper is safe, because the packet holds a reference on
+ * the conntrack object. Never use it from control plane.
+ */
void nf_ct_expect_init(struct nf_conntrack_expect *exp, unsigned int class,
u_int8_t family,
const union nf_inet_addr *saddr,
@@ -325,7 +329,7 @@ void nf_ct_expect_init(struct nf_conntrack_expect *exp, unsigned int class,
exp->flags = 0;
exp->class = class;
exp->expectfn = NULL;
- exp->helper = NULL;
+ rcu_assign_pointer(exp->helper, nfct_help(exp->master)->helper);
exp->tuple.src.l3num = family;
exp->tuple.dst.protonum = proto;
diff --git a/net/netfilter/nf_conntrack_h323_main.c b/net/netfilter/nf_conntrack_h323_main.c
index a2a0e22ccee1..3f5c50455b71 100644
--- a/net/netfilter/nf_conntrack_h323_main.c
+++ b/net/netfilter/nf_conntrack_h323_main.c
@@ -643,7 +643,7 @@ static int expect_h245(struct sk_buff *skb, struct nf_conn *ct,
&ct->tuplehash[!dir].tuple.src.u3,
&ct->tuplehash[!dir].tuple.dst.u3,
IPPROTO_TCP, NULL, &port);
- exp->helper = &nf_conntrack_helper_h245;
+ rcu_assign_pointer(exp->helper, &nf_conntrack_helper_h245);
nathook = rcu_dereference(nfct_h323_nat_hook);
if (memcmp(&ct->tuplehash[dir].tuple.src.u3,
@@ -767,7 +767,7 @@ static int expect_callforwarding(struct sk_buff *skb,
nf_ct_expect_init(exp, NF_CT_EXPECT_CLASS_DEFAULT, nf_ct_l3num(ct),
&ct->tuplehash[!dir].tuple.src.u3, &addr,
IPPROTO_TCP, NULL, &port);
- exp->helper = nf_conntrack_helper_q931;
+ rcu_assign_pointer(exp->helper, nf_conntrack_helper_q931);
nathook = rcu_dereference(nfct_h323_nat_hook);
if (memcmp(&ct->tuplehash[dir].tuple.src.u3,
@@ -1234,7 +1234,7 @@ static int expect_q931(struct sk_buff *skb, struct nf_conn *ct,
&ct->tuplehash[!dir].tuple.src.u3 : NULL,
&ct->tuplehash[!dir].tuple.dst.u3,
IPPROTO_TCP, NULL, &port);
- exp->helper = nf_conntrack_helper_q931;
+ rcu_assign_pointer(exp->helper, nf_conntrack_helper_q931);
exp->flags = NF_CT_EXPECT_PERMANENT; /* Accept multiple calls */
nathook = rcu_dereference(nfct_h323_nat_hook);
@@ -1306,7 +1306,7 @@ static int process_gcf(struct sk_buff *skb, struct nf_conn *ct,
nf_ct_expect_init(exp, NF_CT_EXPECT_CLASS_DEFAULT, nf_ct_l3num(ct),
&ct->tuplehash[!dir].tuple.src.u3, &addr,
IPPROTO_UDP, NULL, &port);
- exp->helper = nf_conntrack_helper_ras;
+ rcu_assign_pointer(exp->helper, nf_conntrack_helper_ras);
if (nf_ct_expect_related(exp, 0) == 0) {
pr_debug("nf_ct_ras: expect RAS ");
@@ -1523,7 +1523,7 @@ static int process_acf(struct sk_buff *skb, struct nf_conn *ct,
&ct->tuplehash[!dir].tuple.src.u3, &addr,
IPPROTO_TCP, NULL, &port);
exp->flags = NF_CT_EXPECT_PERMANENT;
- exp->helper = nf_conntrack_helper_q931;
+ rcu_assign_pointer(exp->helper, nf_conntrack_helper_q931);
if (nf_ct_expect_related(exp, 0) == 0) {
pr_debug("nf_ct_ras: expect Q.931 ");
@@ -1577,7 +1577,7 @@ static int process_lcf(struct sk_buff *skb, struct nf_conn *ct,
&ct->tuplehash[!dir].tuple.src.u3, &addr,
IPPROTO_TCP, NULL, &port);
exp->flags = NF_CT_EXPECT_PERMANENT;
- exp->helper = nf_conntrack_helper_q931;
+ rcu_assign_pointer(exp->helper, nf_conntrack_helper_q931);
if (nf_ct_expect_related(exp, 0) == 0) {
pr_debug("nf_ct_ras: expect Q.931 ");
diff --git a/net/netfilter/nf_conntrack_helper.c b/net/netfilter/nf_conntrack_helper.c
index ceb48c3ca0a4..a4671f517200 100644
--- a/net/netfilter/nf_conntrack_helper.c
+++ b/net/netfilter/nf_conntrack_helper.c
@@ -421,6 +421,11 @@ void nf_conntrack_helper_unregister(struct nf_conntrack_helper *me)
nf_ct_expect_iterate_destroy(expect_iter_me, NULL);
nf_ct_iterate_destroy(unhelp, me);
+
+ /* nf_ct_iterate_destroy() does an unconditional synchronize_rcu() as
+ * last step, this ensures rcu readers of exp->helper are done.
+ * No need for another synchronize_rcu() here.
+ */
}
EXPORT_SYMBOL_GPL(nf_conntrack_helper_unregister);
diff --git a/net/netfilter/nf_conntrack_netlink.c b/net/netfilter/nf_conntrack_netlink.c
index c156574e1273..a42d14290786 100644
--- a/net/netfilter/nf_conntrack_netlink.c
+++ b/net/netfilter/nf_conntrack_netlink.c
@@ -3573,7 +3573,7 @@ ctnetlink_alloc_expect(const struct nlattr * const cda[], struct nf_conn *ct,
exp->class = class;
exp->master = ct;
- exp->helper = helper;
+ rcu_assign_pointer(exp->helper, helper);
exp->tuple = *tuple;
exp->mask.src.u3 = mask->src.u3;
exp->mask.src.u.all = mask->src.u.all;
diff --git a/net/netfilter/nf_conntrack_sip.c b/net/netfilter/nf_conntrack_sip.c
index 4ab5ef71d96d..106b2f419e19 100644
--- a/net/netfilter/nf_conntrack_sip.c
+++ b/net/netfilter/nf_conntrack_sip.c
@@ -1297,7 +1297,7 @@ static int process_register_request(struct sk_buff *skb, unsigned int protoff,
nf_ct_expect_init(exp, SIP_EXPECT_SIGNALLING, nf_ct_l3num(ct),
saddr, &daddr, proto, NULL, &port);
exp->timeout.expires = sip_timeout * HZ;
- exp->helper = helper;
+ rcu_assign_pointer(exp->helper, helper);
exp->flags = NF_CT_EXPECT_PERMANENT | NF_CT_EXPECT_INACTIVE;
hooks = rcu_dereference(nf_nat_sip_hooks);
--
2.47.3
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH nf 2/5] netfilter: nf_conntrack_expect: use expect->helper
2026-03-20 12:59 [PATCH nf 0/5] conntrack expectation fixes Pablo Neira Ayuso
2026-03-20 12:59 ` [PATCH nf 1/5] netfilter: nf_conntrack_expect: honor expectation helper field Pablo Neira Ayuso
@ 2026-03-20 12:59 ` Pablo Neira Ayuso
2026-03-20 12:59 ` [PATCH nf 3/5] netfilter: ctnetlink: ensure safe access to master conntrack Pablo Neira Ayuso
` (2 subsequent siblings)
4 siblings, 0 replies; 7+ messages in thread
From: Pablo Neira Ayuso @ 2026-03-20 12:59 UTC (permalink / raw)
To: netfilter-devel; +Cc: fw
Use expect->helper in ctnetlink and /proc to dump the helper name.
Using nfct_help() without holding a reference to the master conntrack
is unsafe.
Use exp->master->helper in ctnetlink path if userspace does not provide
an explicit helper when creating an expectation to retain the existing
behaviour. The ctnetlink expectation path holds the reference on the
master conntrack and nf_conntrack_expect lock and the nfnetlink glue
path refers to the master ct that is attached to the skb.
Reported-by: Hyunwoo Kim <imv4bel@gmail.com>
Signed-off-by: Pablo Neira Ayuso <pablo@netfilter.org>
---
net/netfilter/nf_conntrack_expect.c | 2 +-
net/netfilter/nf_conntrack_helper.c | 3 +--
net/netfilter/nf_conntrack_netlink.c | 25 ++++++++++---------------
net/netfilter/nf_conntrack_sip.c | 2 +-
4 files changed, 13 insertions(+), 19 deletions(-)
diff --git a/net/netfilter/nf_conntrack_expect.c b/net/netfilter/nf_conntrack_expect.c
index 197a76d0b231..16c7af4044b3 100644
--- a/net/netfilter/nf_conntrack_expect.c
+++ b/net/netfilter/nf_conntrack_expect.c
@@ -658,7 +658,7 @@ static int exp_seq_show(struct seq_file *s, void *v)
if (expect->flags & NF_CT_EXPECT_USERSPACE)
seq_printf(s, "%sUSERSPACE", delim);
- helper = rcu_dereference(nfct_help(expect->master)->helper);
+ helper = rcu_dereference(expect->helper);
if (helper) {
seq_printf(s, "%s%s", expect->flags ? " " : "", helper->name);
if (helper->expect_policy[expect->class].name[0])
diff --git a/net/netfilter/nf_conntrack_helper.c b/net/netfilter/nf_conntrack_helper.c
index a4671f517200..a0c38f5e854b 100644
--- a/net/netfilter/nf_conntrack_helper.c
+++ b/net/netfilter/nf_conntrack_helper.c
@@ -395,14 +395,13 @@ EXPORT_SYMBOL_GPL(nf_conntrack_helper_register);
static bool expect_iter_me(struct nf_conntrack_expect *exp, void *data)
{
- struct nf_conn_help *help = nfct_help(exp->master);
const struct nf_conntrack_helper *me = data;
const struct nf_conntrack_helper *this;
if (exp->helper == me)
return true;
- this = rcu_dereference_protected(help->helper,
+ this = rcu_dereference_protected(exp->helper,
lockdep_is_held(&nf_conntrack_expect_lock));
return this == me;
}
diff --git a/net/netfilter/nf_conntrack_netlink.c b/net/netfilter/nf_conntrack_netlink.c
index a42d14290786..47a073ee2b89 100644
--- a/net/netfilter/nf_conntrack_netlink.c
+++ b/net/netfilter/nf_conntrack_netlink.c
@@ -3012,7 +3012,7 @@ ctnetlink_exp_dump_expect(struct sk_buff *skb,
{
struct nf_conn *master = exp->master;
long timeout = ((long)exp->timeout.expires - (long)jiffies) / HZ;
- struct nf_conn_help *help;
+ struct nf_conntrack_helper __rcu *helper;
#if IS_ENABLED(CONFIG_NF_NAT)
struct nlattr *nest_parms;
struct nf_conntrack_tuple nat_tuple = {};
@@ -3057,15 +3057,13 @@ ctnetlink_exp_dump_expect(struct sk_buff *skb,
nla_put_be32(skb, CTA_EXPECT_FLAGS, htonl(exp->flags)) ||
nla_put_be32(skb, CTA_EXPECT_CLASS, htonl(exp->class)))
goto nla_put_failure;
- help = nfct_help(master);
- if (help) {
- struct nf_conntrack_helper *helper;
- helper = rcu_dereference(help->helper);
- if (helper &&
- nla_put_string(skb, CTA_EXPECT_HELP_NAME, helper->name))
- goto nla_put_failure;
- }
+
+ helper = rcu_dereference(exp->helper);
+ if (helper &&
+ nla_put_string(skb, CTA_EXPECT_HELP_NAME, helper->name))
+ goto nla_put_failure;
+
expfn = nf_ct_helper_expectfn_find_by_symbol(exp->expectfn);
if (expfn != NULL &&
nla_put_string(skb, CTA_EXPECT_FN, expfn->name))
@@ -3393,13 +3391,10 @@ static int ctnetlink_get_expect(struct sk_buff *skb,
static bool expect_iter_name(struct nf_conntrack_expect *exp, void *data)
{
- struct nf_conntrack_helper *helper;
- const struct nf_conn_help *m_help;
+ struct nf_conntrack_helper __rcu *helper;
const char *name = data;
- m_help = nfct_help(exp->master);
-
- helper = rcu_dereference(m_help->helper);
+ helper = rcu_dereference(exp->helper);
if (!helper)
return false;
@@ -3573,7 +3568,7 @@ ctnetlink_alloc_expect(const struct nlattr * const cda[], struct nf_conn *ct,
exp->class = class;
exp->master = ct;
- rcu_assign_pointer(exp->helper, helper);
+ rcu_assign_pointer(exp->helper, helper ? : help->helper);
exp->tuple = *tuple;
exp->mask.src.u3 = mask->src.u3;
exp->mask.src.u.all = mask->src.u.all;
diff --git a/net/netfilter/nf_conntrack_sip.c b/net/netfilter/nf_conntrack_sip.c
index 106b2f419e19..20e57cf5c83a 100644
--- a/net/netfilter/nf_conntrack_sip.c
+++ b/net/netfilter/nf_conntrack_sip.c
@@ -924,7 +924,7 @@ static int set_expected_rtp_rtcp(struct sk_buff *skb, unsigned int protoff,
exp = __nf_ct_expect_find(net, nf_ct_zone(ct), &tuple);
if (!exp || exp->master == ct ||
- nfct_help(exp->master)->helper != nfct_help(ct)->helper ||
+ exp->helper != nfct_help(ct)->helper ||
exp->class != class)
break;
#if IS_ENABLED(CONFIG_NF_NAT)
--
2.47.3
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH nf 3/5] netfilter: ctnetlink: ensure safe access to master conntrack
2026-03-20 12:59 [PATCH nf 0/5] conntrack expectation fixes Pablo Neira Ayuso
2026-03-20 12:59 ` [PATCH nf 1/5] netfilter: nf_conntrack_expect: honor expectation helper field Pablo Neira Ayuso
2026-03-20 12:59 ` [PATCH nf 2/5] netfilter: nf_conntrack_expect: use expect->helper Pablo Neira Ayuso
@ 2026-03-20 12:59 ` Pablo Neira Ayuso
2026-03-20 12:59 ` [PATCH nf 4/5] netfilter: nf_conntrack_expect: store netns and zone in expectation Pablo Neira Ayuso
2026-03-20 12:59 ` [PATCH nf 5/5] netfilter: nf_conntrack_expect: skip expectations in other netns via proc Pablo Neira Ayuso
4 siblings, 0 replies; 7+ messages in thread
From: Pablo Neira Ayuso @ 2026-03-20 12:59 UTC (permalink / raw)
To: netfilter-devel; +Cc: fw
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: Pablo Neira Ayuso <pablo@netfilter.org>
---
include/net/netfilter/nf_conntrack_core.h | 8 +++++++
net/netfilter/nf_conntrack_ecache.c | 2 ++
net/netfilter/nf_conntrack_expect.c | 10 +++++++-
net/netfilter/nf_conntrack_netlink.c | 28 +++++++++++++++--------
4 files changed, 38 insertions(+), 10 deletions(-)
diff --git a/include/net/netfilter/nf_conntrack_core.h b/include/net/netfilter/nf_conntrack_core.h
index 3384859a8921..acc1f05eaedb 100644
--- a/include/net/netfilter/nf_conntrack_core.h
+++ b/include/net/netfilter/nf_conntrack_core.h
@@ -83,6 +83,14 @@ void nf_conntrack_lock(spinlock_t *lock);
extern spinlock_t nf_conntrack_expect_lock;
+static inline void lockdep_nfct_expect_lock_not_held(void)
+{
+#ifdef CONFIG_PROVE_LOCKING
+ if (debug_locks)
+ WARN_ON_ONCE(lockdep_is_held(&nf_conntrack_expect_lock);
+#endif
+}
+
/* 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 81baf2082604..19e060b2856c 100644
--- a/net/netfilter/nf_conntrack_ecache.c
+++ b/net/netfilter/nf_conntrack_ecache.c
@@ -247,6 +247,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_not_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 16c7af4044b3..105d0c39a3c1 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_not_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_not_held();
+
if (timer_delete(&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_not_held();
+
if (!cnet->expect_count)
return NULL;
@@ -446,6 +451,8 @@ static inline int __nf_ct_expect_check(struct nf_conntrack_expect *expect,
unsigned int h;
int ret = 0;
+ lockdep_nfct_expect_lock_not_held();
+
if (!master_help) {
ret = -ESHUTDOWN;
goto out;
@@ -502,8 +509,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 47a073ee2b89..17975fb4905c 100644
--- a/net/netfilter/nf_conntrack_netlink.c
+++ b/net/netfilter/nf_conntrack_netlink.c
@@ -3356,31 +3356,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;
@@ -3427,22 +3433,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 (timer_delete(&exp->timeout)) {
nf_ct_unlink_expect_report(exp, NETLINK_CB(skb).portid,
nlmsg_report(info->nlh));
--
2.47.3
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH nf 4/5] netfilter: nf_conntrack_expect: store netns and zone in expectation
2026-03-20 12:59 [PATCH nf 0/5] conntrack expectation fixes Pablo Neira Ayuso
` (2 preceding siblings ...)
2026-03-20 12:59 ` [PATCH nf 3/5] netfilter: ctnetlink: ensure safe access to master conntrack Pablo Neira Ayuso
@ 2026-03-20 12:59 ` Pablo Neira Ayuso
2026-03-20 13:16 ` Florian Westphal
2026-03-20 12:59 ` [PATCH nf 5/5] netfilter: nf_conntrack_expect: skip expectations in other netns via proc Pablo Neira Ayuso
4 siblings, 1 reply; 7+ messages in thread
From: Pablo Neira Ayuso @ 2026-03-20 12:59 UTC (permalink / raw)
To: netfilter-devel; +Cc: fw
__nf_ct_expect_find() and nf_ct_expect_find_get() are called under
rcu_read_lock() but they dereference the master conntrack via
exp->master.
Since the expectation does not hold a reference on the master conntrack,
this could be dying conntrack or different recycled conntrack than the
real master due to SLAB_TYPESAFE_RCU.
Store the netns, the master_tuple and the zone in struct
nf_conntrack_expect as a safety measure.
This patch is required by the follow up fix not to dump expectations
that do not belong to this netns.
Signed-off-by: Pablo Neira Ayuso <pablo@netfilter.org>
---
include/net/netfilter/nf_conntrack_expect.h | 18 +++++++++++++++++-
net/netfilter/nf_conntrack_expect.c | 8 ++++++--
net/netfilter/nf_conntrack_netlink.c | 5 ++++-
3 files changed, 27 insertions(+), 4 deletions(-)
diff --git a/include/net/netfilter/nf_conntrack_expect.h b/include/net/netfilter/nf_conntrack_expect.h
index 1b01400b10bd..550e03cd2ed1 100644
--- a/include/net/netfilter/nf_conntrack_expect.h
+++ b/include/net/netfilter/nf_conntrack_expect.h
@@ -22,10 +22,16 @@ struct nf_conntrack_expect {
/* Hash member */
struct hlist_node hnode;
+ /* Network namespace */
+ possible_net_t net;
+
/* We expect this tuple, with the following mask */
struct nf_conntrack_tuple tuple;
struct nf_conntrack_tuple_mask mask;
+#ifdef CONFIG_NF_CONNTRACK_ZONES
+ struct nf_conntrack_zone zone;
+#endif
/* Usage count. */
refcount_t use;
@@ -62,7 +68,17 @@ struct nf_conntrack_expect {
static inline struct net *nf_ct_exp_net(struct nf_conntrack_expect *exp)
{
- return nf_ct_net(exp->master);
+ return read_pnet(&exp->net);
+}
+
+static inline bool nf_ct_exp_zone_equal_any(const struct nf_conntrack_expect *a,
+ const struct nf_conntrack_zone *b)
+{
+#ifdef CONFIG_NF_CONNTRACK_ZONES
+ return a->zone.id == b->id;
+#else
+ return true;
+#endif
}
#define NF_CT_EXP_POLICY_NAME_LEN 16
diff --git a/net/netfilter/nf_conntrack_expect.c b/net/netfilter/nf_conntrack_expect.c
index 105d0c39a3c1..4f2c0cb871d7 100644
--- a/net/netfilter/nf_conntrack_expect.c
+++ b/net/netfilter/nf_conntrack_expect.c
@@ -113,8 +113,8 @@ nf_ct_exp_equal(const struct nf_conntrack_tuple *tuple,
const struct net *net)
{
return nf_ct_tuple_mask_cmp(tuple, &i->tuple, &i->mask) &&
- net_eq(net, nf_ct_net(i->master)) &&
- nf_ct_zone_equal_any(i->master, zone);
+ net_eq(net, read_pnet(&i->net)) &&
+ nf_ct_exp_zone_equal_any(i, zone);
}
bool nf_ct_remove_expect(struct nf_conntrack_expect *exp)
@@ -324,6 +324,8 @@ void nf_ct_expect_init(struct nf_conntrack_expect *exp, unsigned int class,
const union nf_inet_addr *daddr,
u_int8_t proto, const __be16 *src, const __be16 *dst)
{
+ struct net *net = read_pnet(&exp->master->ct_net);
+
int len;
if (family == AF_INET)
@@ -335,6 +337,8 @@ void nf_ct_expect_init(struct nf_conntrack_expect *exp, unsigned int class,
exp->class = class;
exp->expectfn = NULL;
rcu_assign_pointer(exp->helper, nfct_help(exp->master)->helper);
+ write_pnet(&exp->net, net);
+ exp->zone = exp->master->zone;
exp->tuple.src.l3num = family;
exp->tuple.dst.protonum = proto;
diff --git a/net/netfilter/nf_conntrack_netlink.c b/net/netfilter/nf_conntrack_netlink.c
index 17975fb4905c..72bbdeb4a2e7 100644
--- a/net/netfilter/nf_conntrack_netlink.c
+++ b/net/netfilter/nf_conntrack_netlink.c
@@ -3539,9 +3539,10 @@ ctnetlink_alloc_expect(const struct nlattr * const cda[], struct nf_conn *ct,
struct nf_conntrack_tuple *tuple,
struct nf_conntrack_tuple *mask)
{
- u_int32_t class = 0;
+ struct net *net = read_pnet(&ct->ct_net);
struct nf_conntrack_expect *exp;
struct nf_conn_help *help;
+ u_int32_t class = 0;
int err;
help = nfct_help(ct);
@@ -3578,6 +3579,8 @@ ctnetlink_alloc_expect(const struct nlattr * const cda[], struct nf_conn *ct,
exp->class = class;
exp->master = ct;
+ write_pnet(&exp->net, net);
+ exp->zone = ct->zone;
rcu_assign_pointer(exp->helper, helper ? : help->helper);
exp->tuple = *tuple;
exp->mask.src.u3 = mask->src.u3;
--
2.47.3
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH nf 5/5] netfilter: nf_conntrack_expect: skip expectations in other netns via proc
2026-03-20 12:59 [PATCH nf 0/5] conntrack expectation fixes Pablo Neira Ayuso
` (3 preceding siblings ...)
2026-03-20 12:59 ` [PATCH nf 4/5] netfilter: nf_conntrack_expect: store netns and zone in expectation Pablo Neira Ayuso
@ 2026-03-20 12:59 ` Pablo Neira Ayuso
4 siblings, 0 replies; 7+ messages in thread
From: Pablo Neira Ayuso @ 2026-03-20 12:59 UTC (permalink / raw)
To: netfilter-devel; +Cc: fw
Skip expectations that do not reside in this netns.
Similar to e77e6ff502ea ("netfilter: conntrack: do not dump other netns's
conntrack entries via proc").
Fixes: 5a1fb391d881 ("netfilter: netns nf_conntrack: add ->ct_net -- pointer from conntrack to netns")
Signed-off-by: Pablo Neira Ayuso <pablo@netfilter.org>
---
net/netfilter/nf_conntrack_expect.c | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/net/netfilter/nf_conntrack_expect.c b/net/netfilter/nf_conntrack_expect.c
index 4f2c0cb871d7..adb542a2407b 100644
--- a/net/netfilter/nf_conntrack_expect.c
+++ b/net/netfilter/nf_conntrack_expect.c
@@ -643,11 +643,15 @@ static int exp_seq_show(struct seq_file *s, void *v)
{
struct nf_conntrack_expect *expect;
struct nf_conntrack_helper *helper;
+ struct net *net = seq_file_net(s);
struct hlist_node *n = v;
char *delim = "";
expect = hlist_entry(n, struct nf_conntrack_expect, hnode);
+ if (!net_eq(nf_ct_exp_net(expect), net))
+ return 0;
+
if (expect->timeout.function)
seq_printf(s, "%ld ", timer_pending(&expect->timeout)
? (long)(expect->timeout.expires - jiffies)/HZ : 0);
--
2.47.3
^ permalink raw reply related [flat|nested] 7+ messages in thread
* Re: [PATCH nf 4/5] netfilter: nf_conntrack_expect: store netns and zone in expectation
2026-03-20 12:59 ` [PATCH nf 4/5] netfilter: nf_conntrack_expect: store netns and zone in expectation Pablo Neira Ayuso
@ 2026-03-20 13:16 ` Florian Westphal
0 siblings, 0 replies; 7+ messages in thread
From: Florian Westphal @ 2026-03-20 13:16 UTC (permalink / raw)
To: Pablo Neira Ayuso; +Cc: netfilter-devel
Pablo Neira Ayuso <pablo@netfilter.org> wrote:
> __nf_ct_expect_find() and nf_ct_expect_find_get() are called under
> rcu_read_lock() but they dereference the master conntrack via
> exp->master.
>
> Since the expectation does not hold a reference on the master conntrack,
> this could be dying conntrack or different recycled conntrack than the
> real master due to SLAB_TYPESAFE_RCU.
Grrr, good point, I was about to say that you can safely check net
via exp->ct netns. But yeah, object recycling is an issue.
I'll push this to nf.git:testing to let build bots have a go at
this over the weekend and will review this more closely next week.
But at first glance this series LGTM, thanks Pablo.
^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-03-20 13:16 UTC | newest]
Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-03-20 12:59 [PATCH nf 0/5] conntrack expectation fixes Pablo Neira Ayuso
2026-03-20 12:59 ` [PATCH nf 1/5] netfilter: nf_conntrack_expect: honor expectation helper field Pablo Neira Ayuso
2026-03-20 12:59 ` [PATCH nf 2/5] netfilter: nf_conntrack_expect: use expect->helper Pablo Neira Ayuso
2026-03-20 12:59 ` [PATCH nf 3/5] netfilter: ctnetlink: ensure safe access to master conntrack Pablo Neira Ayuso
2026-03-20 12:59 ` [PATCH nf 4/5] netfilter: nf_conntrack_expect: store netns and zone in expectation Pablo Neira Ayuso
2026-03-20 13:16 ` Florian Westphal
2026-03-20 12:59 ` [PATCH nf 5/5] netfilter: nf_conntrack_expect: skip expectations in other netns via proc 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