All of lore.kernel.org
 help / color / mirror / Atom feed
From: Pablo Neira Ayuso <pablo@netfilter.org>
To: Jakub Kicinski <kuba@kernel.org>
Cc: netfilter-devel@vger.kernel.org, davem@davemloft.net,
	netdev@vger.kernel.org, pabeni@redhat.com, edumazet@google.com,
	horms@kernel.org, fw@strlen.de, ja@ssi.bg
Subject: Re: [PATCH net-next 09/12] netfilter: nft_ct: move custom expectation support to helper
Date: Wed, 12 Aug 2026 22:41:12 +0200	[thread overview]
Message-ID: <anzaaCW-5UkvPy4B@chamomile> (raw)
In-Reply-To: <20260812013446.1676035-1-kuba@kernel.org>

On Tue, Aug 11, 2026 at 06:34:46PM -0700, Jakub Kicinski wrote:
> This is an AI-generated review of your patch. The human sending this
> email has considered the AI review valid, or at least plausible.
> ---
> netfilter: nft_ct: move custom expectation support to helper
> 
> This moves the nft ct expectation support from creating the expectation
> directly in the object eval path to allocating an internal (unregistered)
> conntrack helper that is attached to the conntrack, so the expectation is
> created from the helper callback at confirmation time. A new exported
> nf_conntrack_helper_release() is added to tear down that internal helper.
> 
> Link: https://patch.msgid.link/20260715144755.00ea7dfcd9f@proton.me
> 
> A few questions below.
> 
> > diff --git a/net/netfilter/nf_conntrack_helper.c b/net/netfilter/nf_conntrack_helper.c
> > index 506c580347615..c30ae3f203bee 100644
> > --- a/net/netfilter/nf_conntrack_helper.c
> > +++ b/net/netfilter/nf_conntrack_helper.c
> > @@ -448,6 +448,15 @@ static bool expect_iter_me(struct nf_conntrack_expect *exp, void *data)
> >  	return this == me;
> >  }
> >  
> > +void nf_conntrack_helper_release(struct nf_conntrack_helper *me)
> > +{
> > +	nf_ct_expect_iterate_destroy(expect_iter_me, me);
> > +
> > +	if (refcount_dec_and_test(&me->ct_refcnt))
> > +		kfree_rcu(me, rcu);
> > +}
> > +EXPORT_SYMBOL_GPL(nf_conntrack_helper_release);
> > +
> >  void nf_conntrack_helper_unregister(struct nf_conntrack_helper *me)
> >  {
> >  	mutex_lock(&nf_ct_helper_mutex);
> > @@ -463,10 +472,7 @@ void nf_conntrack_helper_unregister(struct nf_conntrack_helper *me)
> >  	 */
> >  	synchronize_rcu();
> >  
> > -	nf_ct_expect_iterate_destroy(expect_iter_me, me);
> > -
> > -	if (refcount_dec_and_test(&me->ct_refcnt))
> > -		kfree_rcu(me, rcu);
> > +	nf_conntrack_helper_release(me);
> >  }
> >  EXPORT_SYMBOL_GPL(nf_conntrack_helper_unregister);
> >  
> > diff --git a/net/netfilter/nft_ct.c b/net/netfilter/nft_ct.c
> > index 358b9287e12e8..9dbf127df9c81 100644
> > --- a/net/netfilter/nft_ct.c
> > +++ b/net/netfilter/nft_ct.c
> 
> [ ... ]
> 
> > @@ -1226,6 +1228,93 @@ static int nft_ct_expect_timeout_get(const struct nlattr *attr, u32 *val)
> >  	return 0;
> >  }
> >  
> > +#if IS_ENABLED(CONFIG_NF_NAT)
> > +static void nft_ct_nat_follow_master(struct nf_conn *ct, struct nf_conntrack_expect *this)
> > +{
> > +	const struct nf_ct_helper_expectfn *expfn;
> > +
> > +	expfn = nf_ct_helper_expectfn_find_by_name("nat-follow-master");
> > +	if (expfn)
> > +		expfn->expectfn(ct, this);
> > +}
> > +#endif
> > +
> > +struct nft_ct_expect_data {
> > +	struct nft_ct_expect_obj	obj;
> > +	enum ip_conntrack_dir		dir;
> > +};
> > +
> > +static int ct_expect_help(struct sk_buff *skb, unsigned int protoff,
> > +			  struct nf_conn *ct, enum ip_conntrack_info ctinfo)
> > +{
> > +	enum ip_conntrack_dir dir = CTINFO2DIR(ctinfo);
> > +	struct nft_ct_expect_data *expect_data;
> > +	struct nf_conntrack_expect *exp;
> > +	int ret = NF_ACCEPT;
> > +	u16 l3num;
> > +
> > +	if (nf_ct_is_confirmed(ct))
> > +		return NF_ACCEPT;
> > +
> > +	expect_data = nfct_help_data(ct);
> > +	if (!expect_data)
> > +		return NF_ACCEPT;
> > +
> > +	if (expect_data->dir != dir)
> > +		return NF_ACCEPT;
> > +
> > +	exp = nf_ct_expect_alloc(ct);
> > +	if (!exp)
> > +		return NF_DROP;
> > +
> > +	if (expect_data->obj.l3num == NFPROTO_INET)
> > +		l3num = nf_ct_l3num(ct);
> > +	else
> > +		l3num = expect_data->obj.l3num;
> 
> [Severity: Low]
> This isn't a bug introduced by this patch, but can obj.l3num ever be
> NFPROTO_INET here?  nft_ct_expect_obj_init() rejects it:
> 
> 	case NFPROTO_INET: /* tuple.src.l3num supports NFPROTO_IPV4/6 only */
> 	default:
> 		return -EAFNOSUPPORT;
> 
> so priv->l3num, and therefore the copy in expect_data->obj.l3num, can never
> hold NFPROTO_INET and the nf_ct_l3num(ct) branch looks unreachable.  The same
> code existed in the old nft_ct_expect_obj_eval() and was moved verbatim, so
> this is only a cleanup note: either drop the branch or let init accept
> NFPROTO_INET and resolve it per packet.

Pre-existing issue. As the patch describes, this existing expectation
support usecase is limited to ip and ip6. Not sure I will follow up,
I would like to hear about possible usecases given this is very
limited due to expect_clash() returning EBUSY in case an expectation
already exists.

> > +
> > +	nf_ct_expect_init(exp, NF_CT_EXPECT_CLASS_DEFAULT, l3num,
> > +			  &ct->tuplehash[!dir].tuple.src.u3,
> > +			  &ct->tuplehash[!dir].tuple.dst.u3,
> > +			  expect_data->obj.l4proto, NULL, &expect_data->obj.dport);
> > +	exp->timeout += expect_data->obj.timeout;
> > +
> > +#if IS_ENABLED(CONFIG_NF_NAT)
> > +	if (ct->status & IPS_NAT_MASK) {
> > +		exp->saved_proto.tcp.port = expect_data->obj.dport;
> > +		exp->dir = !dir;
> > +		exp->expectfn = nft_ct_nat_follow_master;
> > +	}
> > +#endif
> > +	if (nf_ct_expect_related(exp, 0) != 0)
> > +		ret = NF_ACCEPT;
> > +
> > +	nf_ct_expect_put(exp);
> > +
> > +	return ret;
> > +}
> > +
> > +static int nft_ct_expect_helper_alloc(struct nft_ct_expect_obj *priv)
> > +{
> > +	struct nf_conntrack_helper *ct_expect_helper;
> > +
> > +	ct_expect_helper = kzalloc_obj(struct nf_conntrack_helper,
> > +				       GFP_KERNEL_ACCOUNT);
> > +	if (!ct_expect_helper)
> > +		return -ENOMEM;
> > +
> > +	snprintf(ct_expect_helper->name, sizeof(ct_expect_helper->name), "%s",
> > +		 "nft_ct_expect");
> > +	ct_expect_helper->me = THIS_MODULE;
> > +	ct_expect_helper->expect_policy[NF_CT_EXPECT_CLASS_DEFAULT].max_expected = priv->size;
> > +	rcu_assign_pointer(ct_expect_helper->help, ct_expect_help);
> > +	refcount_set(&ct_expect_helper->ct_refcnt, 1);
> > +
> > +	/* No need to register this helper, this is internal. */
> > +	priv->helper = ct_expect_helper;
> > +
> > +	return 0;
> > +}
> > +
> 
> [ ... ]
> 
> > @@ -1272,13 +1363,31 @@ static int nft_ct_expect_obj_init(const struct nft_ctx *ctx,
> >  
> >  	priv->dport = nla_get_be16(tb[NFTA_CT_EXPECT_DPORT]);
> >  	priv->size = nla_get_u8(tb[NFTA_CT_EXPECT_SIZE]);
> > +	if (!priv->size)
> > +		priv->size = NF_CT_EXPECT_MAX_CNT;
> 
> [Severity: Medium]
> Does this break the round-trip of NFTA_CT_EXPECT_SIZE?  priv->size is what
> nft_ct_expect_obj_dump() echoes back:
> 
> 	    nla_put_u8(skb, NFTA_CT_EXPECT_SIZE, priv->size))
> 
> so an object created with size 0 is now listed as size 255.  The policy
> [NFTA_CT_EXPECT_SIZE] = { .type = NLA_U8 } accepts 0, so this is reachable
> from userspace.
> 
> The meaning of size 0 also flips: the pre-patch eval tested
> 
> 	if (help->expecting[NF_CT_EXPECT_CLASS_DEFAULT] >= priv->size)
> 
> which is always true for 0, i.e. size 0 meant "never create expectations",
> whereas it now means "allow the maximum".
> 
> Would it work to apply the substitution only where it is needed, i.e. to
> expect_policy[NF_CT_EXPECT_CLASS_DEFAULT].max_expected in
> nft_ct_expect_helper_alloc(), and keep the user-supplied value in
> priv->size?  That is what __nf_conntrack_helper_register() does, but there
> the clamped value lives in an internal policy struct that is never sent to
> userspace.
> 
> Related: with the new scheme ct_expect_help() only runs while the conntrack
> is unconfirmed and only for the single recorded direction, so at most one
> expectation per conntrack is created and the size attribute has no
> observable effect anymore.

This is explaining the existing limitations in this nft_ct expectation
support.

The main goal with this patch is to address a real issue in the
conntrack core as the patch describes, which is more important than
this feature.

> > +
> > +	err = nf_ct_netns_get(ctx->net, ctx->family);
> > +	if (err < 0)
> > +		return err;
> >  
> > -	return nf_ct_netns_get(ctx->net, ctx->family);
> > +	err = nft_ct_expect_helper_alloc(priv);
> > +	if (err < 0) {
> > +		nf_ct_netns_put(ctx->net, ctx->family);
> > +		return err;
> > +	}
> > +
> > +	return err;
> >  }
> >  
> >  static void nft_ct_expect_obj_destroy(const struct nft_ctx *ctx,
> > -				       struct nft_object *obj)
> > +				      struct nft_object *obj)
> >  {
> > +	const struct nft_ct_expect_obj *priv = nft_obj_data(obj);
> > +	struct nf_conntrack_helper *me = priv->helper;
> > +
> > +	/* This helper is going away, disable it. */
> > +	rcu_assign_pointer(me->help, NULL);
> > +	nf_conntrack_helper_release(me);
> >  	nf_ct_netns_put(ctx->net, ctx->family);
> >  }
> 
> [Severity: High]
> Is a grace period needed between clearing me->help and sweeping the
> expectation table here?  nf_conntrack_helper_unregister() does exactly these
> two steps with a synchronize_rcu() in between:
> 
> 	rcu_assign_pointer(me->help, NULL);
> 
> 	/* Make sure every nothing is still using the helper unless its a
> 	 * connection in the hash.
> 	 */
> 	synchronize_rcu();
> 
> 	nf_conntrack_helper_release(me);
> 
> The refactor left the grace period in the unregister wrapper, so the new
> external caller does not get it.
> 
> Can an in-flight ct_expect_help() then insert an expectation after the sweep
> has finished?
> 
> CPU A (softirq), nf_confirm():
> 	helper_cb = rcu_dereference(helper->help);	/* still ct_expect_help */
> 	...
> 	ret = helper_cb(skb, protoff, ct, ctinfo);
> 	  ct_expect_help()
> 	    nf_ct_expect_alloc() / nf_ct_expect_init()
> 
> CPU B (process context), object delete:
> 	nft_ct_expect_obj_destroy()
> 	  rcu_assign_pointer(me->help, NULL);
> 	  nf_conntrack_helper_release()
> 	    nf_ct_expect_iterate_destroy(expect_iter_me, me);  /* sweeps */
> 
> CPU A continues:
> 	    nf_ct_expect_related(exp, 0)	/* takes nf_conntrack_expect_lock
> 						   after the sweep, inserts */
> 
> rcu_assign_pointer() does not wait for a reader that already loaded the old
> pointer, and the callback is reached through the conntrack's own
> help->helper in nf_confirm() (and nf_ct_helper() in nf_conntrack_ovs.c), not
> through the nft object, so the nf_tables commit-time synchronize_rcu() that
> precedes ops->destroy() does not fence it.
> 
> Since this sweep is the only teardown for these expectations, would the
> escaped expectation (exp->helper == me, and exp->expectfn ==
> nft_ct_nat_follow_master when NAT is in use) then keep admitting a RELATED
> connection after the object/rule is gone, until its timeout expires or the
> master conntrack dies?
> 
> The commit message says:
> 
>     The nft_ct module removal always waits for rcu grace period, then the
>     NULL helper callback is observed after this.
> 
> but nft_ct_expect_obj_destroy() itself does not wait; object destruction is
> not module removal.  Would keeping the unregister ordering here (or folding
> the synchronize_rcu() into nf_conntrack_helper_release()) be preferable?

I can follow harden this, to kill existing expectations on module
removal for nft_ct and nfnetlink_cthelper, this race is unlikely to
happen but it is possible on module removal.

> > @@ -1297,27 +1406,14 @@ static int nft_ct_expect_obj_dump(struct sk_buff *skb,
> 
> [ ... ]
> 
> >  static void nft_ct_expect_obj_eval(struct nft_object *obj,
> >  				   struct nft_regs *regs,
> >  				   const struct nft_pktinfo *pkt)
> >  {
> >  	const struct nft_ct_expect_obj *priv = nft_obj_data(obj);
> > -	struct nf_conntrack_expect *exp;
> > +	struct nft_ct_expect_data *expect_data;
> >  	enum ip_conntrack_info ctinfo;
> >  	struct nf_conn_help *help;
> > -	enum ip_conntrack_dir dir;
> > -	u16 l3num = priv->l3num;
> >  	struct nf_conn *ct;
> >  
> >  	ct = nf_ct_get(pkt->skb, &ctinfo);
> > @@ -1325,45 +1421,30 @@ static void nft_ct_expect_obj_eval(struct nft_object *obj,
> >  		regs->verdict.code = NFT_BREAK;
> >  		return;
> >  	}
> > -	dir = CTINFO2DIR(ctinfo);
> >  
> >  	help = nfct_help(ct);
> > -	if (!help)
> > -		help = nf_ct_helper_ext_add(ct, GFP_ATOMIC);
> > -	if (!help) {
> > -		regs->verdict.code = NF_DROP;
> > -		return;
> > -	}
> > -
> > -	if (help->expecting[NF_CT_EXPECT_CLASS_DEFAULT] >= priv->size) {
> > +	if (help) {
> >  		regs->verdict.code = NFT_BREAK;
> >  		return;
> >  	}
> 
> [Severity: Medium]
> Is this change of behaviour intended?  The old code reused an existing
> nf_conn_help extension; now any conntrack that already carries one gives up
> with NFT_BREAK and no expectation is created at all.
> 
> A help extension can already be present on an unconfirmed conntrack from
> several places, for example:
> 
>   - nft_ct_helper_obj_eval() calling nf_ct_helper_ext_add() for a preceding
>     "ct helper set" statement
>   - __nf_ct_try_assign_helper() from init_conntrack()
>   - the exp->assign_helper path in init_conntrack():
> 
> 	assign_helper = rcu_dereference(exp->assign_helper);
> 	if (assign_helper) {
> 		help = nf_ct_helper_ext_add(ct, GFP_ATOMIC);
> 
>   - ctnetlink created conntracks
>   - a second "ct expectation set" statement
> 
> Since NFT_BREAK aborts the whole rule, do the statements following
> "ct expectation set" (counter/log/accept/jump) get skipped too, so such
> packets now fall through to the next rule or the chain policy?
> 
> Also, on the commit message:
> 
>     I'd rather not increase the size of the struct nf_conn for this feature
>     has very limited scope: only one expectation can be created at a time
>     given expect_clash() will make nf_ct_expect_related() reports EBUSY.
> 
> Is that accurate for the pre-patch code?  expect_clash() goes through
> nf_ct_tuple_mask_cmp(), which compares the destination tuple including
> dst.u.all, so two expectation objects with different dports on the same
> master did not clash and could coexist up to priv->size.  Should the commit
> message describe the single-expectation limit as something this patch
> introduces rather than as existing behaviour?
> 
> > -	if (l3num == NFPROTO_INET)
> > -		l3num = nf_ct_l3num(ct);
> >  
> > -	exp = nf_ct_expect_alloc(ct);
> > -	if (exp == NULL) {
> > +	help = nf_ct_helper_ext_add(ct, GFP_ATOMIC);
> > +	if (!help) {
> >  		regs->verdict.code = NF_DROP;
> >  		return;
> >  	}
> 
> [ ... ]
> 
> > +	expect_data = nfct_help_data(ct);
> > +	if (!expect_data) {
> > +		regs->verdict.code = NFT_BREAK;
> > +		return;
> >  	}
> > -#endif
> > -	if (nf_ct_expect_related(exp, 0) != 0)
> > -		regs->verdict.code = NF_DROP;
> > +	expect_data->obj = *priv;
> > +	expect_data->obj.helper = NULL;
> > +	expect_data->dir = CTINFO2DIR(ctinfo);
> >  
> > -	nf_ct_expect_put(exp);
> > +	if (help && refcount_inc_not_zero(&priv->helper->ct_refcnt))
> > +		rcu_assign_pointer(help->helper, priv->helper);
> >  }

  reply	other threads:[~2026-08-12 20:41 UTC|newest]

Thread overview: 23+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-10 19:40 [PATCH net-next 00/12] Netfilter updates for net Pablo Neira Ayuso
2026-08-10 19:40 ` [PATCH net-next 01/12] netfilter: add DEBUG_NET_WARN_ON_ONCE to skb_set_nfct() Pablo Neira Ayuso
2026-08-12  1:34   ` Jakub Kicinski
2026-08-12  7:20     ` Pablo Neira Ayuso
2026-08-10 19:40 ` [PATCH net-next 02/12] net: pass net_device_path_ctx to dev_fill_forward_path() Pablo Neira Ayuso
2026-08-10 19:40 ` [PATCH net-next 03/12] net: netfilter: add ether_type to net_device_path_ctx and use it Pablo Neira Ayuso
2026-08-12  1:34   ` Jakub Kicinski
2026-08-12 20:34     ` Pablo Neira Ayuso
2026-08-10 19:40 ` [PATCH net-next 04/12] netfilter: flowtable: rename tun.l3_proto to tun.inner_proto Pablo Neira Ayuso
2026-08-10 19:40 ` [PATCH net-next 05/12] netfilter: flowtable: rename ctx.tun.proto to ctx.tun.inner_proto Pablo Neira Ayuso
2026-08-10 19:40 ` [PATCH net-next 06/12] netfilter: flowtable: store ethertype in flowtable context Pablo Neira Ayuso
2026-08-12  1:34   ` Jakub Kicinski
2026-08-12 20:36     ` Pablo Neira Ayuso
2026-08-10 19:40 ` [PATCH net-next 07/12] netfilter: flowtable: move ipv4 and ipv6 xmit path to function Pablo Neira Ayuso
2026-08-10 19:40 ` [PATCH net-next 08/12] netfilter: flowtable: detach layer 2 encapsulation parser from lookup Pablo Neira Ayuso
2026-08-10 19:40 ` [PATCH net-next 09/12] netfilter: nft_ct: move custom expectation support to helper Pablo Neira Ayuso
2026-08-12  1:34   ` Jakub Kicinski
2026-08-12 20:41     ` Pablo Neira Ayuso [this message]
2026-08-10 19:40 ` [PATCH net-next 10/12] netfilter: conntrack: always lower timeout for non-closing RST packets Pablo Neira Ayuso
2026-08-10 19:40 ` [PATCH net-next 11/12] netfilter: nf_conntrack_expect: bail out on insert dead expectations Pablo Neira Ayuso
2026-08-12  1:34   ` Jakub Kicinski
2026-08-12 20:41     ` Pablo Neira Ayuso
2026-08-10 19:40 ` [PATCH net-next 12/12] selftests: netfilter: conntrack_dump_flush: remove unused variables and fix typo Pablo Neira Ayuso

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=anzaaCW-5UkvPy4B@chamomile \
    --to=pablo@netfilter.org \
    --cc=davem@davemloft.net \
    --cc=edumazet@google.com \
    --cc=fw@strlen.de \
    --cc=horms@kernel.org \
    --cc=ja@ssi.bg \
    --cc=kuba@kernel.org \
    --cc=netdev@vger.kernel.org \
    --cc=netfilter-devel@vger.kernel.org \
    --cc=pabeni@redhat.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
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.