mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH nf] netfilter: ipset: fix double free on kernel-side delete during resize
@ 2026-10-12 16:33 Xue Boyang
  2026-10-14 12:50 ` Jozsef Kadlecsik
  0 siblings, 1 reply; 4+ messages in thread
From: Xue Boyang @ 2026-10-12 16:33 UTC (permalink / raw)
  To: Jozsef Kadlecsik, Pablo Neira Ayuso, Florian Westphal
  Cc: netfilter-devel, coreteam, netdev, linux-kernel, Phil Sutter

A kernel-side delete, i.e. a delete triggered by the SET target while
the set is being resized, is applied to the current table and saved in
the resize backlog, to be replayed against the new table when the
resize finished. Both tables share the element extensions: the resize
copies the element to the new table, keeping the pointer to the
extension data, e.g. to the comment. Deleting the element in the
current table calls ip_set_ext_destroy() and releases the shared
extension. The replay then deletes the copy from the new table and
releases the same extension again, i.e. the same rcu_head is queued
twice. With CONFIG_DEBUG_OBJECTS_RCU_HEAD (and slub_debug=F, which
keeps kfree_rcu() off the sheaf fast path):

  ODEBUG: activate active (active state 1) object type: rcu_head
  WARNING: lib/debugobjects.c:629 at debug_print_object+0x7b/0x100
  kvfree_call_rcu(): Double-freed call. rcu_head ffff88810474ce40
  WARNING: mm/slab_common.c:2041 at kvfree_call_rcu+0x10b/0x140
  Call Trace:
   <TASK>
   debug_object_activate (lib/debugobjects.c:905)
   kvfree_call_rcu (mm/slab_common.c:2039)
   hash_ip4_del (net/netfilter/ipset/ip_set_hash_gen.h:1226)
   hash_ip4_kadt (net/netfilter/ipset/ip_set_hash_ip.c:96)
   ip_set_del (net/netfilter/ipset/ip_set_core.c:820)
   set_target_v3 (net/netfilter/xt_set.c:411)
   ipt_do_table (net/ipv4/netfilter/ip_tables.c:345)
   nf_hook_slow (net/netfilter/core.c:619)
   raw_sendmsg (net/ipv4/raw.c:652)
   __sys_sendto (net/socket.c:2281)

The double free is reachable from an unprivileged user namespace and
permits local privilege escalation.

Fix this by deferring the delete when a resize is in progress: the
element is not deleted in the current table, only saved in the backlog,
like it is done for kernel-side adds. The resize copies the element
including its extension pointers, and the replayed delete releases the
extension exactly once. This also avoids updating the cidr counters
twice for the same element.

Deferred deletes of a failed resize are replayed against the original
table, which stays the live table, instead of being dropped.

Fixes: f66ee0410b1c ("netfilter: ipset: Fix "INFO: rcu detected stall in hash_xxx" reports")
Cc: stable@vger.kernel.org
Signed-off-by: Xue Boyang <fuchen.dust@gmail.com>
Assisted-by: LLM DeepSeek-V4.1-Flash
---

The reproducer (SET --del-set rule plus a userspace add triggering the
resize) was run against v7.3-rc5 (mainline tree at 6d25ffca055a):
before the patch the double free shows up as the warning quoted in the
commit message and as duplicate objects in the SLUB RCU sheaves; with
the patch applied it no longer reproduces in repeated runs. The
reproducer is not attached; it is available and can be shared privately
upon request. Note: the replay of deferred deletes on a resize abort
(allocation failure in the copy loop) is covered by review only.

Found with AI assistance (DeepSeek V4.1 Flash, see Assisted-by); the
patch was reviewed, reproduced and tested by hand.

 net/netfilter/ipset/ip_set_hash_gen.h | 48 +++++++++++++++++----------
 1 file changed, 31 insertions(+), 17 deletions(-)

diff --git a/net/netfilter/ipset/ip_set_hash_gen.h b/net/netfilter/ipset/ip_set_hash_gen.h
index 80ca523f304b..d94db604ed6b 100644
--- a/net/netfilter/ipset/ip_set_hash_gen.h
+++ b/net/netfilter/ipset/ip_set_hash_gen.h
@@ -764,6 +764,7 @@ mtype_resize(struct ip_set *set, bool retried)
 	struct hbucket *n, *m;
 	struct list_head *l, *lt;
 	struct mtype_resize_ad *x;
+	LIST_HEAD(backlog);
 	u32 i, j, r, nr, key;
 	int ret;
 
@@ -934,13 +935,21 @@ mtype_resize(struct ip_set *set, bool retried)
 	if (ret == -EAGAIN)
 		goto retry;
 
-	/* Cleanup the backlog of ADD/DEL elements */
+	/* Cleanup the backlog of ADD/DEL elements: kernel-side adds were
+	 * applied to the original table, which is still the live table,
+	 * so they are simply dropped. Kernel-side deletes were deferred,
+	 * replay them against the original table.
+	 */
 	spin_lock_bh(&set->lock);
-	list_for_each_safe(l, lt, &orig->ad) {
+	list_splice_init(&orig->ad, &backlog);
+	spin_unlock_bh(&set->lock);
+	list_for_each_safe(l, lt, &backlog) {
+		x = list_entry(l, struct mtype_resize_ad, list);
 		list_del(l);
-		kfree(l);
+		if (x->ad == IPSET_DEL)
+			mtype_del(set, &x->d, NULL, NULL, 0);
+		kfree(x);
 	}
-	spin_unlock_bh(&set->lock);
 	goto out;
 
 hbwarn:
@@ -1217,6 +1226,24 @@ mtype_del(struct ip_set *set, void *value, const struct ip_set_ext *ext,
 			goto out;
 
 		ret = 0;
+		if (t->resizing && ext && ext->target) {
+			/* Resize is in process and kernel side del: do not
+			 * delete the element and free its extensions here,
+			 * the extensions may be shared with the copy in
+			 * the new table. Save the values, the resize replays
+			 * the delete against the new table.
+			 */
+			x = kzalloc_obj(struct mtype_resize_ad, GFP_ATOMIC);
+			if (!x)
+				/* Don't bother, deleting the element here
+				 * could free the shared extensions twice.
+				 */
+				goto out;
+			x->ad = IPSET_DEL;
+			memcpy(&x->d, value, sizeof(struct mtype_elem));
+			x->flags = flags;
+			goto out;
+		}
 		clear_bit(i, n->used);
 		smp_mb__after_atomic();
 		if (i + 1 == pos)
@@ -1224,19 +1251,6 @@ mtype_del(struct ip_set *set, void *value, const struct ip_set_ext *ext,
 		t->hregion[r].elements--;
 		mtype_del_cidr_all(set, h, d);
 		ip_set_ext_destroy(set, data);
-
-		if (t->resizing && ext && ext->target) {
-			/* Resize is in process and kernel side del,
-			 * save values
-			 */
-			x = kzalloc_obj(struct mtype_resize_ad, GFP_ATOMIC);
-			if (x) {
-				x->ad = IPSET_DEL;
-				memcpy(&x->d, value,
-				       sizeof(struct mtype_elem));
-				x->flags = flags;
-			}
-		}
 		for (; i < pos; i++) {
 			if (!test_bit(i, n->used))
 				k++;
-- 
2.53.0


^ permalink raw reply	[flat|nested] 4+ messages in thread

* Re: [PATCH nf] netfilter: ipset: fix double free on kernel-side delete during resize
  2026-10-12 16:33 [PATCH nf] netfilter: ipset: fix double free on kernel-side delete during resize Xue Boyang
@ 2026-10-14 12:50 ` Jozsef Kadlecsik
  2026-10-25 14:07   ` Xue Boyang
  0 siblings, 1 reply; 4+ messages in thread
From: Jozsef Kadlecsik @ 2026-10-14 12:50 UTC (permalink / raw)
  To: Xue Boyang
  Cc: Jozsef Kadlecsik, Pablo Neira Ayuso, Florian Westphal,
	netfilter-devel, coreteam, netdev, linux-kernel, Phil Sutter

Hi,

On Mon, 12 Oct 2026, Xue Boyang wrote:

> A kernel-side delete, i.e. a delete triggered by the SET target while
> the set is being resized, is applied to the current table and saved in
> the resize backlog, to be replayed against the new table when the
> resize finished. Both tables share the element extensions: the resize
> copies the element to the new table, keeping the pointer to the
> extension data, e.g. to the comment. Deleting the element in the
> current table calls ip_set_ext_destroy() and releases the shared
> extension. The replay then deletes the copy from the new table and
> releases the same extension again, i.e. the same rcu_head is queued
> twice. With CONFIG_DEBUG_OBJECTS_RCU_HEAD (and slub_debug=F, which
> keeps kfree_rcu() off the sheaf fast path):
>
>  ODEBUG: activate active (active state 1) object type: rcu_head
>  WARNING: lib/debugobjects.c:629 at debug_print_object+0x7b/0x100
>  kvfree_call_rcu(): Double-freed call. rcu_head ffff88810474ce40
>  WARNING: mm/slab_common.c:2041 at kvfree_call_rcu+0x10b/0x140
>  Call Trace:
>   <TASK>
>   debug_object_activate (lib/debugobjects.c:905)
>   kvfree_call_rcu (mm/slab_common.c:2039)
>   hash_ip4_del (net/netfilter/ipset/ip_set_hash_gen.h:1226)
>   hash_ip4_kadt (net/netfilter/ipset/ip_set_hash_ip.c:96)
>   ip_set_del (net/netfilter/ipset/ip_set_core.c:820)
>   set_target_v3 (net/netfilter/xt_set.c:411)
>   ipt_do_table (net/ipv4/netfilter/ip_tables.c:345)
>   nf_hook_slow (net/netfilter/core.c:619)
>   raw_sendmsg (net/ipv4/raw.c:652)
>   __sys_sendto (net/socket.c:2281)
>
> The double free is reachable from an unprivileged user namespace and
> permits local privilege escalation.
>
> Fix this by deferring the delete when a resize is in progress: the
> element is not deleted in the current table, only saved in the backlog,
> like it is done for kernel-side adds. The resize copies the element
> including its extension pointers, and the replayed delete releases the
> extension exactly once. This also avoids updating the cidr counters
> twice for the same element.
>
> Deferred deletes of a failed resize are replayed against the original
> table, which stays the live table, instead of being dropped.

Thank you for your patch! Could you address the issues sashiko raised at 
https://sashiko.dev/#/patchset/20261012163353.4093-1-fuchen.dust%40gmail.com 
about it?

Best regards,
Jozsef

> Fixes: f66ee0410b1c ("netfilter: ipset: Fix "INFO: rcu detected stall in hash_xxx" reports")
> Cc: stable@vger.kernel.org
> Signed-off-by: Xue Boyang <fuchen.dust@gmail.com>
> Assisted-by: LLM DeepSeek-V4.1-Flash
> ---
>
> The reproducer (SET --del-set rule plus a userspace add triggering the
> resize) was run against v7.3-rc5 (mainline tree at 6d25ffca055a):
> before the patch the double free shows up as the warning quoted in the
> commit message and as duplicate objects in the SLUB RCU sheaves; with
> the patch applied it no longer reproduces in repeated runs. The
> reproducer is not attached; it is available and can be shared privately
> upon request. Note: the replay of deferred deletes on a resize abort
> (allocation failure in the copy loop) is covered by review only.
>
> Found with AI assistance (DeepSeek V4.1 Flash, see Assisted-by); the
> patch was reviewed, reproduced and tested by hand.
>
> net/netfilter/ipset/ip_set_hash_gen.h | 48 +++++++++++++++++----------
> 1 file changed, 31 insertions(+), 17 deletions(-)
>
> diff --git a/net/netfilter/ipset/ip_set_hash_gen.h b/net/netfilter/ipset/ip_set_hash_gen.h
> index 80ca523f304b..d94db604ed6b 100644
> --- a/net/netfilter/ipset/ip_set_hash_gen.h
> +++ b/net/netfilter/ipset/ip_set_hash_gen.h
> @@ -764,6 +764,7 @@ mtype_resize(struct ip_set *set, bool retried)
> 	struct hbucket *n, *m;
> 	struct list_head *l, *lt;
> 	struct mtype_resize_ad *x;
> +	LIST_HEAD(backlog);
> 	u32 i, j, r, nr, key;
> 	int ret;
>
> @@ -934,13 +935,21 @@ mtype_resize(struct ip_set *set, bool retried)
> 	if (ret == -EAGAIN)
> 		goto retry;
>
> -	/* Cleanup the backlog of ADD/DEL elements */
> +	/* Cleanup the backlog of ADD/DEL elements: kernel-side adds were
> +	 * applied to the original table, which is still the live table,
> +	 * so they are simply dropped. Kernel-side deletes were deferred,
> +	 * replay them against the original table.
> +	 */
> 	spin_lock_bh(&set->lock);
> -	list_for_each_safe(l, lt, &orig->ad) {
> +	list_splice_init(&orig->ad, &backlog);
> +	spin_unlock_bh(&set->lock);
> +	list_for_each_safe(l, lt, &backlog) {
> +		x = list_entry(l, struct mtype_resize_ad, list);
> 		list_del(l);
> -		kfree(l);
> +		if (x->ad == IPSET_DEL)
> +			mtype_del(set, &x->d, NULL, NULL, 0);
> +		kfree(x);
> 	}
> -	spin_unlock_bh(&set->lock);
> 	goto out;
>
> hbwarn:
> @@ -1217,6 +1226,24 @@ mtype_del(struct ip_set *set, void *value, const struct ip_set_ext *ext,
> 			goto out;
>
> 		ret = 0;
> +		if (t->resizing && ext && ext->target) {
> +			/* Resize is in process and kernel side del: do not
> +			 * delete the element and free its extensions here,
> +			 * the extensions may be shared with the copy in
> +			 * the new table. Save the values, the resize replays
> +			 * the delete against the new table.
> +			 */
> +			x = kzalloc_obj(struct mtype_resize_ad, GFP_ATOMIC);
> +			if (!x)
> +				/* Don't bother, deleting the element here
> +				 * could free the shared extensions twice.
> +				 */
> +				goto out;
> +			x->ad = IPSET_DEL;
> +			memcpy(&x->d, value, sizeof(struct mtype_elem));
> +			x->flags = flags;
> +			goto out;
> +		}
> 		clear_bit(i, n->used);
> 		smp_mb__after_atomic();
> 		if (i + 1 == pos)
> @@ -1224,19 +1251,6 @@ mtype_del(struct ip_set *set, void *value, const struct ip_set_ext *ext,
> 		t->hregion[r].elements--;
> 		mtype_del_cidr_all(set, h, d);
> 		ip_set_ext_destroy(set, data);
> -
> -		if (t->resizing && ext && ext->target) {
> -			/* Resize is in process and kernel side del,
> -			 * save values
> -			 */
> -			x = kzalloc_obj(struct mtype_resize_ad, GFP_ATOMIC);
> -			if (x) {
> -				x->ad = IPSET_DEL;
> -				memcpy(&x->d, value,
> -				       sizeof(struct mtype_elem));
> -				x->flags = flags;
> -			}
> -		}
> 		for (; i < pos; i++) {
> 			if (!test_bit(i, n->used))
> 				k++;
> -- 
> 2.53.0
>
>

^ permalink raw reply	[flat|nested] 4+ messages in thread

* Re: [PATCH nf] netfilter: ipset: fix double free on kernel-side delete during resize
  2026-10-14 12:50 ` Jozsef Kadlecsik
@ 2026-10-25 14:07   ` Xue Boyang
  2026-10-25 14:07     ` [PATCH nf v2] " Xue Boyang
  0 siblings, 1 reply; 4+ messages in thread
From: Xue Boyang @ 2026-10-25 14:07 UTC (permalink / raw)
  To: Jozsef Kadlecsik
  Cc: Xue Boyang, netfilter-devel, coreteam, netdev, linux-kernel, Phil Sutter

Hi Jozsef,

thanks for looking at the patch!

> Thank you for your patch! Could you address the issues sashiko raised at
> https://sashiko.dev/#/patchset/20261012163353.4093-1-fuchen.dust%40gmail.com
> about it?

Sure - v2 follows right after this mail, quoting the findings and what I
did about each of them:

> Can a memory allocation failure bypass the backlog cleanup block and leak
> the deferred operations? [Medium]

The exact path claimed cannot: every early exit is taken before the table
is marked resizing, so the backlog is still empty there. The leak is real
one retry later though: on -EAGAIN the cleanup path jumps to the retry
before draining the backlog, and an early failure of the next attempt
(its own allocation failures are again before the mark) strands it - and
with the deferred delete being the only record of the delete, the
elements would stay in the set while the packet path believes them
deleted. v2 drains the backlog on all failure exits, replaying it
against the original table.

> Does the backlog replay ordering during resize incorrectly resurrect
> deleted elements? [High]

Yes, it does - the list_add() prepend of deferred deletes predates this
patch. Since the fix now relies on the backlog order, v2 queues the
deferred deletes with list_add_tail() as well, so the whole backlog
replays in arrival order, on the failure paths too.

> Does this skip clear_bit(i, n->used) and leave the element fully active
> in the original table? If a subsequent ADD for the same element occurs
> before the resize completes, will mtype_add() incorrectly see the
> element as still existing, return -IPSET_ERR_EXIST, and fail to queue
> its own operation to the backlog? [High]

Exactly, and fixed the other way around: clearing the used bit is not an
option, the extension of an element the resize copy loop has not reached
yet would never be freed (the old table is destroyed with
ext_destroy=false to protect the extensions shared with the copied
elements). v2 defers the kernel-side add of an element still present in
the current table instead, so the replay applies the del and the add in
arrival order and the add is no longer lost.

The remaining Medium concern (leak of the extensions of elements which
expired during the resize) is preexisting and not touched by v2 - I can
look at it separately. The five dismissed concerns look like false
positives to me, e.g. the lockless orig->ad walk after the table swap is
serialized by the synchronize_rcu().

Also included in v2: the deferred delete now reports -ENOMEM when its
backlog entry cannot be allocated, instead of returning success.

v2 was tested with the same reproducer as v1 (no double free in repeated
runs) plus a new ordering regression test: packet-path del-then-add
pairs race continuous resizes and the set membership is checked right
after a resize completes, with the traffic source frozen so nothing can
paper over the outcome. On v1 the element deleted and re-added during
the window comes out missing (the lost add); an instrumented v1 kernel
confirmed a single ~80ms resize window deferring 253 packet-path
deletes and dropping the corresponding adds with -EEXIST, replaying the
deletes in LIFO order. On v2 the element is present in every sampled
resize across repeated runs.

Best regards,
Xue Boyang

^ permalink raw reply	[flat|nested] 4+ messages in thread

* [PATCH nf v2] netfilter: ipset: fix double free on kernel-side delete during resize
  2026-10-25 14:07   ` Xue Boyang
@ 2026-10-25 14:07     ` Xue Boyang
  0 siblings, 0 replies; 4+ messages in thread
From: Xue Boyang @ 2026-10-25 14:07 UTC (permalink / raw)
  To: kadlec
  Cc: netfilter-devel, coreteam, netdev, linux-kernel, phil,
	Xue Boyang, stable

A kernel-side delete, i.e. a delete triggered by the SET target while
the set is being resized, is applied to the current table and saved in
the resize backlog, to be replayed against the new table when the
resize finished. Both tables share the element extensions: the resize
copies the element to the new table, keeping the pointer to the
extension data, e.g. to the comment. Deleting the element in the
current table calls ip_set_ext_destroy() and releases the shared
extension. The replay then deletes the copy from the new table and
releases the same extension again, i.e. the same rcu_head is queued
twice. With CONFIG_DEBUG_OBJECTS_RCU_HEAD (and slub_debug=F, which
keeps kfree_rcu() off the sheaf fast path):

  ODEBUG: activate active (active state 1) object type: rcu_head
  WARNING: lib/debugobjects.c:629 at debug_print_object+0x7b/0x100
  kvfree_call_rcu(): Double-freed call. rcu_head ffff88810474ce40
  WARNING: mm/slab_common.c:2041 at kvfree_call_rcu+0x10b/0x140
  Call Trace:
   <TASK>
   debug_object_activate (lib/debugobjects.c:905)
   kvfree_call_rcu (mm/slab_common.c:2039)
   hash_ip4_del (net/netfilter/ipset/ip_set_hash_gen.h:1226)
   hash_ip4_kadt (net/netfilter/ipset/ip_set_hash_ip.c:96)
   ip_set_del (net/netfilter/ipset/ip_set_core.c:820)
   set_target_v3 (net/netfilter/xt_set.c:411)
   ipt_do_table (net/ipv4/netfilter/ip_tables.c:345)
   nf_hook_slow (net/netfilter/core.c:619)
   raw_sendmsg (net/ipv4/raw.c:652)
   __sys_sendto (net/socket.c:2281)

The double free is reachable from an unprivileged user namespace and
permits local privilege escalation.

Fix this by deferring the delete when a resize is in progress: the
element is not deleted in the current table, only saved in the backlog,
like it is done for kernel-side adds. The resize copies the element
including its extension pointers, and the replayed delete releases the
extension exactly once. This also avoids updating the cidr counters
twice for the same element.

Clearing the used bit of the deferred delete instead is not an option:
the extension of an element the resize copy loop has not reached yet
would never be freed, the old table is destroyed with ext_destroy=false
to protect the extensions shared with the copied elements. The element
thus stays in the current table until the resize completes, so

- a kernel-side add of such an element must be deferred too, otherwise
  it is rejected with -EEXIST and lost, and the replayed delete removes
  the element for good, leaving the set without an element the last
  packet-path operation added;

- the backlog must keep the arrival order of the operations: deferred
  deletes are queued with list_add_tail() like the deferred adds, so an
  add followed by a delete of the same element is not replayed the
  other way around, resurrecting the element.

A resize which is aborted before the new table is in place replays the
backlog against the original table, which stays the live table, instead
of dropping it. This covers the allocation failure in the copy loop and
the early exits of a retry after -EAGAIN, which inherits the backlog of
the previous attempt: the deferred deletes are the only record of the
delete there, dropping them would leave elements in the set which the
packet path believes deleted, and the backlog entries would leak.

The deferred delete reports -ENOMEM when the backlog entry cannot be
allocated, instead of returning success without deleting anything.

Fixes: f66ee0410b1c ("netfilter: ipset: Fix "INFO: rcu detected stall in hash_xxx" reports")
Cc: stable@vger.kernel.org
Signed-off-by: Xue Boyang <fuchen.dust@gmail.com>
Assisted-by: LLM DeepSeek-V4.1-Flash
---

Changes in v2, addressing the review of v1 linked in
https://sashiko.dev/#/patchset/20261012163353.4093-1-fuchen.dust%40gmail.com :

- Defer the kernel-side add of an element still present in the current
  table during the resize ("missing clear_bit() breaks operation
  ordering during resize"). The add was rejected with -EEXIST without
  being queued, so an add following a deferred delete was lost and the
  replayed delete removed the element.

- Replay the backlog on all resize failure exits, not only on the
  allocation failure in the copy loop ("memory leak of deferred
  operations on resize failure"). The exact path claimed there, an
  allocation failure of the first attempt, exits before the table is
  marked resizing and the backlog is empty; the leak is real however
  for the early exits of a retry after -EAGAIN, which inherits a
  non-empty backlog from the previous attempt.

- Queue the deferred deletes with list_add_tail() ("backlog replay
  ordering during resize resurrects deleted elements"). The prepend
  predates this patch, but the fix relies on the backlog preserving
  the arrival order, so all deferred operations are now queued FIFO
  and the failure path replays the full backlog in order instead of
  dropping the deferred adds.

- Report -ENOMEM when the deferred delete cannot allocate its backlog
  entry.

The "memory leak of extensions for expired elements" concern is
preexisting (the resize copy loop skips expired elements and the table
destroy keeps ext_destroy=false) and is not addressed here.

The reproducer (SET --del-set rule plus a userspace add triggering the
resize) was run against v7.3-rc5 (mainline tree at 6d25ffca055a):
before the patch the double free shows up as the warning quoted in the
commit message and as duplicate objects in the SLUB RCU sheaves; with
the patch applied it no longer reproduces in repeated runs. The
reproducer is not attached; it is available and can be shared privately
upon request. The ordering of the deferred operations and the replay of
the backlog were verified with a packet-path del+add / add+del
regression test racing continuous resizes: v1 shows the lost add and
the resurrected element, v2 shows neither in repeated runs.

Found with AI assistance (DeepSeek V4.1 Flash, see Assisted-by); the
patch was reviewed, reproduced and tested by hand.
---
 net/netfilter/ipset/ip_set_hash_gen.h | 77 +++++++++++++++++++--------
 1 file changed, 56 insertions(+), 21 deletions(-)

diff --git a/net/netfilter/ipset/ip_set_hash_gen.h b/net/netfilter/ipset/ip_set_hash_gen.h
index 80ca523f304b..f24c51d489e2 100644
--- a/net/netfilter/ipset/ip_set_hash_gen.h
+++ b/net/netfilter/ipset/ip_set_hash_gen.h
@@ -764,6 +764,7 @@ mtype_resize(struct ip_set *set, bool retried)
 	struct hbucket *n, *m;
 	struct list_head *l, *lt;
 	struct mtype_resize_ad *x;
+	LIST_HEAD(backlog);
 	u32 i, j, r, nr, key;
 	int ret;
 
@@ -786,13 +787,13 @@ mtype_resize(struct ip_set *set, bool retried)
 	t = ip_set_alloc(hsize);
 	if (!t) {
 		ret = -ENOMEM;
-		goto out;
+		goto drain;
 	}
 	t->hregion = ip_set_alloc(ahash_sizeof_regions(htable_bits));
 	if (!t->hregion) {
 		ip_set_free(t);
 		ret = -ENOMEM;
-		goto out;
+		goto drain;
 	}
 	t->htable_bits = htable_bits;
 	t->maxelem = h->maxelem / ahash_numof_locks(htable_bits);
@@ -934,20 +935,34 @@ mtype_resize(struct ip_set *set, bool retried)
 	if (ret == -EAGAIN)
 		goto retry;
 
-	/* Cleanup the backlog of ADD/DEL elements */
+drain:
+	/* Cleanup the backlog of ADD/DEL elements: replay them in arrival
+	 * order against the original table, which is still the live table.
+	 * Kernel-side adds of elements still present are no-ops, the
+	 * deferred deletes remove the elements. This is also reached when
+	 * the resize fails early, e.g. on allocation failure in a retry
+	 * after -EAGAIN: the backlog may still hold deferred operations
+	 * from the previous attempt.
+	 */
 	spin_lock_bh(&set->lock);
-	list_for_each_safe(l, lt, &orig->ad) {
+	list_splice_init(&orig->ad, &backlog);
+	spin_unlock_bh(&set->lock);
+	list_for_each_safe(l, lt, &backlog) {
+		x = list_entry(l, struct mtype_resize_ad, list);
 		list_del(l);
-		kfree(l);
+		if (x->ad == IPSET_ADD)
+			mtype_add(set, &x->d, &x->ext, &x->mext, x->flags);
+		else
+			mtype_del(set, &x->d, NULL, NULL, 0);
+		kfree(x);
 	}
-	spin_unlock_bh(&set->lock);
 	goto out;
 
 hbwarn:
 	/* In case we have plenty of memory :-) */
 	pr_warn("Cannot increase the hashsize of set %s further\n", set->name);
 	ret = -IPSET_ERR_HASH_FULL;
-	goto out;
+	goto drain;
 }
 
 /* Get the current number of elements and ext_size in the set  */
@@ -1053,6 +1068,19 @@ mtype_add(struct ip_set *set, void *value, const struct ip_set_ext *ext,
 				j = i;
 				goto overwrite_extensions;
 			}
+			if (t->resizing && ext && ext->target) {
+				/* Resize is in progress and kernel side add
+				 * of an existing element: the element may
+				 * have a deferred delete pending in the
+				 * backlog and it is going to be copied to
+				 * the new table, sharing the extensions.
+				 * Defer the add too, so that the replay of
+				 * the backlog after resize keeps the order
+				 * of the operations.
+				 */
+				ret = 0;
+				goto resize;
+			}
 			ret = -IPSET_ERR_EXIST;
 			goto unlock;
 		}
@@ -1217,6 +1245,26 @@ mtype_del(struct ip_set *set, void *value, const struct ip_set_ext *ext,
 			goto out;
 
 		ret = 0;
+		if (t->resizing && ext && ext->target) {
+			/* Resize is in process and kernel side del: do not
+			 * delete the element and free its extensions here,
+			 * the extensions may be shared with the copy in
+			 * the new table. Save the values, the resize replays
+			 * the delete against the new table.
+			 */
+			x = kzalloc_obj(struct mtype_resize_ad, GFP_ATOMIC);
+			if (!x) {
+				/* Don't bother, deleting the element here
+				 * could free the shared extensions twice.
+				 */
+				ret = -ENOMEM;
+				goto out;
+			}
+			x->ad = IPSET_DEL;
+			memcpy(&x->d, value, sizeof(struct mtype_elem));
+			x->flags = flags;
+			goto out;
+		}
 		clear_bit(i, n->used);
 		smp_mb__after_atomic();
 		if (i + 1 == pos)
@@ -1224,19 +1272,6 @@ mtype_del(struct ip_set *set, void *value, const struct ip_set_ext *ext,
 		t->hregion[r].elements--;
 		mtype_del_cidr_all(set, h, d);
 		ip_set_ext_destroy(set, data);
-
-		if (t->resizing && ext && ext->target) {
-			/* Resize is in process and kernel side del,
-			 * save values
-			 */
-			x = kzalloc_obj(struct mtype_resize_ad, GFP_ATOMIC);
-			if (x) {
-				x->ad = IPSET_DEL;
-				memcpy(&x->d, value,
-				       sizeof(struct mtype_elem));
-				x->flags = flags;
-			}
-		}
 		for (; i < pos; i++) {
 			if (!test_bit(i, n->used))
 				k++;
@@ -1273,7 +1308,7 @@ mtype_del(struct ip_set *set, void *value, const struct ip_set_ext *ext,
 	spin_unlock_bh(&t->hregion[r].lock);
 	if (x) {
 		spin_lock_bh(&set->lock);
-		list_add(&x->list, &t->ad);
+		list_add_tail(&x->list, &t->ad);
 		spin_unlock_bh(&set->lock);
 	}
 	if (atomic_dec_and_test(&t->uref) && t->resizing) {
-- 
2.53.0


^ permalink raw reply	[flat|nested] 4+ messages in thread

end of thread, other threads:[~2026-10-25 14:07 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-12 16:33 [PATCH nf] netfilter: ipset: fix double free on kernel-side delete during resize Xue Boyang
2026-10-14 12:50 ` Jozsef Kadlecsik
2026-10-25 14:07   ` Xue Boyang
2026-10-25 14:07     ` [PATCH nf v2] " Xue Boyang

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®