Re: [PATCH bpf] bpf: Fix netns reference imbalance in conntrack kfuncs

"Emil Tsalapatis" <[email protected]> Thu, 30 Jul 2026 02:38:18 -0400
Newsgroups gmane.linux.network,gmane.comp.security.firewalls.netfilter.devel,gmane.linux.kernel,gmane.linux.kernel.bpf
Message-ID <[email protected]>
On Wed Jul 29, 2026 at 12:31 PM EDT, Chengfeng Ye wrote:
> The opts argument of the BPF conntrack kfuncs can point to a shared map
> value.  __bpf_nf_ct_lookup() and __bpf_nf_ct_alloc_entry() read
> opts->netns_id separately when acquiring and releasing the network
> namespace reference.
>
> The reference imbalance can occur as follows:
>
>   CPU 0                                  CPU 1
>   read opts->netns_id (-1)
>   skip get_net_ns_by_id()
>                                          write opts->netns_id (id)
>   read opts->netns_id (id)
>   put_net(net) /* no matching get */
>
> The reverse transition leaks the reference.  Repeating the unmatched put
> can destroy a live namespace and crash later users.
>
> The kernel reported:
>
>   Oops: general protection fault, probably for non-canonical address
>   KASAN: null-ptr-deref in range [0x00000000000000e8-0x00000000000000ef]
>   RIP: 0010:bpf_prog_test_run_xdp+0x52c/0x1700
>   Call Trace:
>    __sys_bpf+0x1662/0x50c0
>    __x64_sys_bpf+0x73/0xb0
>    do_syscall_64+0xf9/0x540
>    entry_SYSCALL_64_after_hwframe+0x77/0x7f
>   Kernel panic - not syncing: Fatal exception
>
> Read netns_id once with READ_ONCE() and use that value for validation
> and the matching get/put pair.  Each invocation then consistently uses
> either the calling namespace or a referenced namespace.
>
> Fixes: aed8ee7feb44 ("net: netfilter: Deduplicate code in bpf_{xdp,skb}_ct_lookup")
> Fixes: d7e79c97c00c ("net: netfilter: Add kfuncs to allocate and insert CT")
> Signed-off-by: Chengfeng Ye <[email protected]>

Sashiko is right on this regarding all other fields of opts having the
same issue. While they can't lead to a crash, let's still snapshot them
at the beginning of the function so they don't change from under us.
This is still far from ideal - we may read an inconsistent snapshot of
the struct - but it's backed by BPF program-controlled memory so AFAICT
this is the best we can do.

pw-bot: cr

> ---
> Please queue this fix for stable kernels.
>
>  net/netfilter/nf_conntrack_bpf.c | 20 ++++++++++++--------
>  1 file changed, 12 insertions(+), 8 deletions(-)
>
> diff --git a/net/netfilter/nf_conntrack_bpf.c b/net/netfilter/nf_conntrack_bpf.c
> index f98d1d4b42c3..8b540846f299 100644
> --- a/net/netfilter/nf_conntrack_bpf.c
> +++ b/net/netfilter/nf_conntrack_bpf.c
> @@ -122,6 +122,7 @@ __bpf_nf_ct_alloc_entry(struct net *net, struct bpf_sock_tuple *bpf_tuple,
>  	struct nf_conntrack_tuple otuple, rtuple;
>  	struct nf_conntrack_zone ct_zone;
>  	struct nf_conn *ct;
> +	s32 netns_id;
>  	int err;
>  
>  	if (!(opts_len == NF_BPF_CT_OPTS_SZ || opts_len == 12))
> @@ -134,7 +135,8 @@ __bpf_nf_ct_alloc_entry(struct net *net, struct bpf_sock_tuple *bpf_tuple,
>  			return ERR_PTR(-EINVAL);
>  	}
>  
> -	if (unlikely(opts->netns_id < BPF_F_CURRENT_NETNS))
> +	netns_id = READ_ONCE(opts->netns_id);
> +	if (unlikely(netns_id < BPF_F_CURRENT_NETNS))
>  		return ERR_PTR(-EINVAL);
>  
>  	err = bpf_nf_ct_tuple_parse(bpf_tuple, tuple_len, opts->l4proto,
> @@ -147,8 +149,8 @@ __bpf_nf_ct_alloc_entry(struct net *net, struct bpf_sock_tuple *bpf_tuple,
>  	if (err < 0)
>  		return ERR_PTR(err);
>  
> -	if (opts->netns_id >= 0) {
> -		net = get_net_ns_by_id(net, opts->netns_id);
> +	if (netns_id >= 0) {
> +		net = get_net_ns_by_id(net, netns_id);
>  		if (unlikely(!net))
>  			return ERR_PTR(-ENONET);
>  	}
> @@ -171,7 +173,7 @@ __bpf_nf_ct_alloc_entry(struct net *net, struct bpf_sock_tuple *bpf_tuple,
>  	__nf_ct_set_timeout(ct, timeout * HZ);
>  
>  out:
> -	if (opts->netns_id >= 0)
> +	if (netns_id >= 0)
>  		put_net(net);
>  
>  	return ct;
> @@ -186,6 +188,7 @@ static struct nf_conn *__bpf_nf_ct_lookup(struct net *net,
>  	struct nf_conntrack_tuple tuple;
>  	struct nf_conntrack_zone ct_zone;
>  	struct nf_conn *ct;
> +	s32 netns_id;
>  	int err;
>  
>  	if (!opts || !bpf_tuple)
> @@ -201,7 +204,8 @@ static struct nf_conn *__bpf_nf_ct_lookup(struct net *net,
>  	}
>  	if (unlikely(opts->l4proto != IPPROTO_TCP && opts->l4proto != IPPROTO_UDP))
>  		return ERR_PTR(-EPROTO);
> -	if (unlikely(opts->netns_id < BPF_F_CURRENT_NETNS))
> +	netns_id = READ_ONCE(opts->netns_id);
> +	if (unlikely(netns_id < BPF_F_CURRENT_NETNS))
>  		return ERR_PTR(-EINVAL);
>  
>  	err = bpf_nf_ct_tuple_parse(bpf_tuple, tuple_len, opts->l4proto,
> @@ -209,8 +213,8 @@ static struct nf_conn *__bpf_nf_ct_lookup(struct net *net,
>  	if (err < 0)
>  		return ERR_PTR(err);
>  
> -	if (opts->netns_id >= 0) {
> -		net = get_net_ns_by_id(net, opts->netns_id);
> +	if (netns_id >= 0) {
> +		net = get_net_ns_by_id(net, netns_id);
>  		if (unlikely(!net))
>  			return ERR_PTR(-ENONET);
>  	}
> @@ -225,7 +229,7 @@ static struct nf_conn *__bpf_nf_ct_lookup(struct net *net,
>  	}
>  
>  	hash = nf_conntrack_find_get(net, &ct_zone, &tuple);
> -	if (opts->netns_id >= 0)
> +	if (netns_id >= 0)
>  		put_net(net);
>  	if (!hash)
>  		return ERR_PTR(-ENOENT);