Re: [PATCH nf 1/7] netfilter: ipset: remove need to allocate memory on delete operations

Jozsef Kadlecsik <[email protected]>
Newsgroups gmane.comp.security.firewalls.netfilter.devel
Message-ID <[email protected]>
Hi Florian,

On Thu, 6 Aug 2026, Florian Westphal wrote:

> Allocating mem via GFP_ATOMIC on delete is problematic, delete operations
> should always succeed.
> 
> Do in-place substitution:  When /cidr reaches 0 count (no more elements in
> the range), move ranges stored later in the array forward and keep the
> count 0 ones at the end.
> 
> INIT_CIDR() can then check count == 0 without a need to search next element
> in the array.
> 
> To avoid problems on weakly ordered architectures, pack the structure so it
> is only 32bit wide, then use READ/WRITE_ONCE to store both cidr and count.
> atomically.
> 
> Also update comments to mention the possible presence of ignored
> 0-count-0-cidr structures at the end and need for seqcount.
> 
> seqcount is used to restart.  This avoids bogus range misses.
> Given:  [0]: /29 [1]: /24
> cpu1 reads slot 0. then, right after, cpu2 removes /29. count drops to 0,
> so it updates array to: [0], /24, [1], /0 (count 0).
> 
> cpu1 then skips /28: slot 0 was already seen and slot 1 already replaced.

Sigh. It's getting more and more complicated to maintain a fairly simple 
data structure.

What do you think which is better for the test (match) case (not 
add/del), performance-wise:

- seqcount and retry
- holes left [and INIT_CIDR() converted to a function to skip the holes
  and find the first entry for add/del]

Best regards,
Jozsef
 
> Assisted-by: Claude:claude-sonnet-5
> Fixes: 8e5fd2a55e24 ("netfilter: ipset: rework cidr bookkeeping")
> Signed-off-by: Florian Westphal <[email protected]>
> ---
>  Was not part of earlier RFC series.
> 
>  net/netfilter/ipset/ip_set_hash_gen.h        | 149 ++++++++++++++-----
>  net/netfilter/ipset/ip_set_hash_netiface.c   |   1 -
>  net/netfilter/ipset/ip_set_hash_netportnet.c |   1 -
>  3 files changed, 113 insertions(+), 38 deletions(-)
> 
> diff --git a/net/netfilter/ipset/ip_set_hash_gen.h b/net/netfilter/ipset/ip_set_hash_gen.h
> index f00c82acd7f0..8c79938410a0 100644
> --- a/net/netfilter/ipset/ip_set_hash_gen.h
> +++ b/net/netfilter/ipset/ip_set_hash_gen.h
> @@ -8,6 +8,7 @@
>  #include <linux/rcupdate_wait.h>
>  #include <linux/jhash.h>
>  #include <linux/types.h>
> +#include <linux/seqlock.h>
>  #include <linux/netfilter/nfnetlink.h>
>  #include <linux/netfilter/ipset/ip_set.h>
>  
> @@ -98,14 +99,34 @@ struct htable {
>  #define IPSET_NET_COUNT		1
>  #endif
>  
> -/* Book-keeping of the prefixes added to the set */
> +/**
> + * struct net_prefix - Representation of a network prefix.
> + * @cidr: The CIDR prefix length.
> + * @count: Number of occurrences.
> + */
>  struct net_prefix {
> -	u8 cidr;			/* the cidr value */
> -	u32 count;			/* number of elements of this cidr */
> +	u32 cidr:8;
> +	u32 count:24;
>  };
>  
> +#define CIDR_MAX_COUNT ((1 << 24) - 1)
> +
> +/**
> + * struct net_prefixes - A collection of network prefixes.
> + * @rcu: RCU head
> + * @seq: Sequence counter guarding in-place reordering of @nets
> + * @len: Number of entries in the array.
> + * @nets: Array of net_prefix structures (sorted by CIDR descending).
> + *
> + * @nets entries are updated in place under @set's lock. A single entry's
> + * cidr/count pair is always updated atomically via READ_ONCE()/WRITE_ONCE(),
> + * but removing an entry also shifts every following entry down by one slot.
> + * Lockless readers that scan the whole array (i.e. more than a single
> + * indexed slot) must use @seq to detect and retry across such a shift.
> + */
>  struct net_prefixes {
>  	struct rcu_head rcu;
> +	seqcount_spinlock_t seq;
>  	u8 len;
>  	struct net_prefix nets[] __counted_by(len);
>  };
> @@ -143,8 +164,11 @@ htable_size(u8 hbits)
>  #endif
>  
>  #define INIT_CIDR(n, host_mask) ({				\
> -	const struct net_prefixes *__n = rcu_dereference(n);		\
> -	DCIDR_PUT((__n)->len ? (__n)->nets[0].cidr : host_mask);\
> +	const struct net_prefixes *__n = rcu_dereference(n);	\
> +	struct net_prefix __p =					\
> +		__n->len ? READ_ONCE(__n->nets[0])		\
> +			 : (struct net_prefix){};		\
> +	DCIDR_PUT(__p.count ? __p.cidr : host_mask);		\
>  })
>  
>  #endif /* IP_SET_HASH_WITH_NETS */
> @@ -318,27 +342,43 @@ struct mtype_resize_ad {
>  };
>  
>  #ifdef IP_SET_HASH_WITH_NETS
> -/* Network cidr size book keeping when the hash stores different
> - * sized networks. cidr == real cidr + 1 to support /0.
> +/**
> + * mtype_add_cidr - Add a CIDR entry to hash table bookkeeping
> + * @set: Pointer to the ip_set
> + * @h: Pointer to the htype
> + * @cidr: The CIDR prefix length
> + * @n: The index of the net_prefix array to add @cidr to
> + *
> + * Performs an update if @cidr is found, otherwise performs COW-style
> + * allocation and replacement via RCU.
> + *
> + * Return: 0 on success, negative error code on failure.
>   */
>  static int
>  mtype_add_cidr(struct ip_set *set, struct htype *h, u8 cidr, u8 n)
>  {
> -	struct net_prefixes *nets, *tmp;
>  	int i, j, found, len = 0, ret = 0;
> +	struct net_prefixes *nets, *tmp;
> +	struct net_prefix np;
>  
>  	spin_lock_bh(&set->lock);
>  	nets = __ipset_dereference(h->rnets[n]);
>  	/* Add in increasing prefix order, so larger cidr first */
>  	for (i = 0, found = -1; i < nets->len; i++) {
> -		if (nets->nets[i].count)
> +		np = READ_ONCE(nets->nets[i]);
> +		if (np.count)
>  			len++;
>  		if (found != -1) {
>  			continue;
> -		} else if (nets->nets[i].cidr < cidr) {
> +		} else if (np.cidr < cidr) {
>  			found = i;
> -		} else if (nets->nets[i].cidr == cidr) {
> -			nets->nets[i].count++;
> +		} else if (np.cidr == cidr) {
> +			if (np.count < CIDR_MAX_COUNT) {
> +				np.count++;
> +				WRITE_ONCE(nets->nets[i], np);
> +			} else {
> +				ret = -EOVERFLOW;
> +			}
>  			goto unlock;
>  		}
>  	}
> @@ -350,6 +390,7 @@ mtype_add_cidr(struct ip_set *set, struct htype *h, u8 cidr, u8 n)
>  	}
>  
>  	tmp->len = len;
> +	seqcount_spinlock_init(&tmp->seq, &set->lock);
>  	for (i = 0, j = 0; i < nets->len; i++) {
>  		if (i == found) {
>  			tmp->nets[j].cidr = cidr;
> @@ -371,42 +412,60 @@ mtype_add_cidr(struct ip_set *set, struct htype *h, u8 cidr, u8 n)
>  	return ret;
>  }
>  
> +/**
> + * mtype_del_cidr - Remove CIDR entry and maintain array integrity.
> + * @set: Pointer to the ip_set.
> + * @h: Pointer to the htype.
> + * @cidr: The CIDR prefix length.
> + * @n: The index of the net_prefix array to remove @cidr from
> + *
> + * If CIDR entry count falls to 0, this function performs a "shift-left"
> + * operation on all following elements. This ensures that the array remains
> + * contiguous and maintains its descending order by CIDR. The vacated slot
> + * at the end of the array is zeroed out (cidr=0, count=0).
> + */
>  static void
>  mtype_del_cidr(struct ip_set *set, struct htype *h, u8 cidr, u8 n)
>  {
> -	struct net_prefixes *nets, *tmp;
> -	u8 i, j, len = 0;
> +	struct net_prefixes *nets;
> +	struct net_prefix np;
>  	int found;
> +	u8 i, j;
> +
> +	BUILD_BUG_ON(sizeof(struct net_prefix) != sizeof(u32));
>  
>  	spin_lock_bh(&set->lock);
>  	nets = __ipset_dereference(h->rnets[n]);
>  	for (i = 0, found = -1; i < nets->len; i++) {
> -		if (nets->nets[i].count)
> -			len++;
> -		if (nets->nets[i].cidr == cidr)
> +		np = READ_ONCE(nets->nets[i]);
> +		if (np.count && np.cidr == cidr) {
> +			np.count--;
>  			found = i;
> +			break;
> +		}
>  	}
>  	if (unlikely(found == -1))
>  		goto unlock;
>  
> -	nets->nets[found].count--;
> -	if (nets->nets[found].count)
> -		goto unlock;
> -	len--;
> -	tmp = kzalloc_flex(*tmp, nets, len, GFP_ATOMIC);
> -	if (!tmp)
> -		/* Leave a hole */
> +	if (np.count) {
> +		WRITE_ONCE(nets->nets[found], np);
>  		goto unlock;
> +	}
>  
> -	tmp->len = len;
> +	write_seqcount_begin(&nets->seq);
>  	for (i = 0, j = 0; i < nets->len; i++) {
> -		if (!nets->nets[i].count || i == found)
> +		if (i == found)
>  			continue;
> -		tmp->nets[j].cidr = nets->nets[i].cidr;
> -		tmp->nets[j++].count = nets->nets[i].count;
> +
> +		np = READ_ONCE(nets->nets[i]);
> +		if (i != j)
> +			WRITE_ONCE(nets->nets[j], np);
> +		j++;
>  	}
> -	rcu_assign_pointer(h->rnets[n], tmp);
> -	kfree_rcu(nets, rcu);
> +
> +	while (j < nets->len)
> +		WRITE_ONCE(nets->nets[j++], (struct net_prefix){});
> +	write_seqcount_end(&nets->seq);
>  unlock:
>  	spin_unlock_bh(&set->lock);
>  }
> @@ -1253,31 +1312,41 @@ mtype_test_cidrs(struct ip_set *set, struct mtype_elem *d,
>  #if IPSET_NET_COUNT == 2
>  	struct net_prefixes *nets1;
>  	struct mtype_elem orig = *d;
> +	unsigned int seq1;
>  	int ret, i, j, k;
>  #else
>  	int ret, i, j;
>  #endif
> -	u32 key, multi = 0;
> +	unsigned int seq0;
> +	u32 key, multi;
>  	u8 pos;
>  
>  	pr_debug("test by nets\n");
>  	rcu_read_lock_bh();
> +retry:
> +	multi = 0;
>  	nets0 = rcu_dereference_bh(h->rnets[0]);
> +	seq0 = read_seqcount_begin(&nets0->seq);
>  #if IPSET_NET_COUNT == 2
>  	nets1 = rcu_dereference_bh(h->rnets[1]);
> +	seq1 = read_seqcount_begin(&nets1->seq);
>  #endif
>  	for (j = 0; j < nets0->len && !multi; j++) {
> -		if (!nets0->nets[j].count)
> +		struct net_prefix p0 = READ_ONCE(nets0->nets[j]);
> +
> +		if (!p0.count)
>  			continue;
>  #if IPSET_NET_COUNT == 2
>  		mtype_data_reset_elem(d, &orig);
> -		mtype_data_netmask(d, nets0->nets[j].cidr, false);
> +		mtype_data_netmask(d, p0.cidr, false);
>  		for (k = 0; k < nets1->len && !multi; k++) {
> -			if (!nets1->nets[k].count)
> +			struct net_prefix p1 = READ_ONCE(nets1->nets[k]);
> +
> +			if (!p1.count)
>  				continue;
> -			mtype_data_netmask(d, nets1->nets[k].cidr, true);
> +			mtype_data_netmask(d, p1.cidr, true);
>  #else
> -		mtype_data_netmask(d, nets0->nets[j].cidr);
> +		mtype_data_netmask(d, p0.cidr);
>  #endif
>  		key = HKEY(d, h->initval, t->htable_bits);
>  		n = rcu_dereference_bh(hbucket(t, key));
> @@ -1302,6 +1371,13 @@ mtype_test_cidrs(struct ip_set *set, struct mtype_elem *d,
>  		}
>  #endif
>  	}
> +
> +	if (read_seqcount_retry(&nets0->seq, seq0))
> +		goto retry;
> +#if IPSET_NET_COUNT == 2
> +	if (read_seqcount_retry(&nets1->seq, seq1))
> +		goto retry;
> +#endif
>  	ret = 0;
>  unlock:
>  	rcu_read_unlock_bh();
> @@ -1707,6 +1783,7 @@ IPSET_TOKEN(HTYPE, _create)(struct net *net, struct ip_set *set,
>  				kfree(rcu_dereference_raw(h->rnets[--i]));
>  			goto free_hregion;
>  		}
> +		seqcount_spinlock_init(&nets->seq, &set->lock);
>  		RCU_INIT_POINTER(h->rnets[i], nets);
>  	}
>  #endif
> diff --git a/net/netfilter/ipset/ip_set_hash_netiface.c b/net/netfilter/ipset/ip_set_hash_netiface.c
> index b44b95f766b7..b602cc43565d 100644
> --- a/net/netfilter/ipset/ip_set_hash_netiface.c
> +++ b/net/netfilter/ipset/ip_set_hash_netiface.c
> @@ -38,7 +38,6 @@ MODULE_ALIAS("ip_set_hash:net,iface");
>  #define HTYPE		hash_netiface
>  #define IP_SET_HASH_WITH_NETS
>  #define IP_SET_HASH_WITH_MULTI
> -#define IP_SET_HASH_WITH_NET0
>  
>  #define STRSCPY(a, b)	strscpy(a, b, IFNAMSIZ)
>  
> diff --git a/net/netfilter/ipset/ip_set_hash_netportnet.c b/net/netfilter/ipset/ip_set_hash_netportnet.c
> index 6291532be7a5..61af1ce27127 100644
> --- a/net/netfilter/ipset/ip_set_hash_netportnet.c
> +++ b/net/netfilter/ipset/ip_set_hash_netportnet.c
> @@ -36,7 +36,6 @@ MODULE_ALIAS("ip_set_hash:net,port,net");
>  #define IP_SET_HASH_WITH_PROTO
>  #define IP_SET_HASH_WITH_NETS
>  #define IPSET_NET_COUNT 2
> -#define IP_SET_HASH_WITH_NET0
>  
>  /* IPv4 variant */
>  
> -- 
> 2.54.0
> 
> 

-- 
E-mail : [email protected], [email protected], [email protected]
Address: Wigner Research Centre for Physics
         H-1525 Budapest 114, POB. 49, Hungary
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.