[PATCH nf v4 2/3] ipvs: avoid stack overflow from recursive connection expiration

From: Zihan Xi

Date: Wed Sep 23 2026 - 05:56:17 EST


When a controlled IPVS connection expires, its controller may be expired
synchronously if it has no remaining controlled connections. A chain of
controlled connections can then recurse through ip_vs_conn_expire() and
exhaust the kernel stack during namespace cleanup.

Continue expiration with the controller after the current connection has
been fully cleaned up instead of calling ip_vs_conn_del() recursively. Keep
the expiration walk under RCU, preserve the immediate-drop timeout for a
controller with its own controller, and switch to deletion mode before the
next iteration.

This keeps controlled-connection cleanup synchronous while using one stack
frame for the whole chain. The timer callback race during connection
deletion is handled by the preceding refcount fix.

Fixes: f9200a52eedf ("ipvs: avoid expiring many connections from timer")
Cc: stable@xxxxxxxxxxxxxxx
Reported-by: Vega <vega@xxxxxxxxxx>
Assisted-by: LLM
Co-developed-by: Luxing Yin <root@xxxxxxxxxx>
Signed-off-by: Luxing Yin <root@xxxxxxxxxx>
Signed-off-by: Zihan Xi <zihanx@xxxxxxxxxx>
---
changes in v4:
- Rebase the iterative controller cleanup on the timer-callback fix.
- Keep the controller walk synchronous and switch to deletion mode for
the next iteration.
- v3 Link:
https://lore.kernel.org/all/cover.1789877273.git.zihanx@xxxxxxxxxx/
changes in v3:
- Handle the timer-callback race while keeping controller cleanup
iterative and synchronous.
- v2 Link:
https://lore.kernel.org/all/cover.1789435989.git.zihanx@xxxxxxxxxx/
changes in v2:
- Replace recursive controller expiration with an iterative repeat path.
- v1 Link:
https://lore.kernel.org/all/cover.1789110326.git.zihanx@xxxxxxxxxx/

net/netfilter/ipvs/ip_vs_conn.c | 20 ++++++++++++++++----
1 file changed, 16 insertions(+), 4 deletions(-)

diff --git a/net/netfilter/ipvs/ip_vs_conn.c b/net/netfilter/ipvs/ip_vs_conn.c
index 32cfc02aa2912..f85752e79ed92 100644
--- a/net/netfilter/ipvs/ip_vs_conn.c
+++ b/net/netfilter/ipvs/ip_vs_conn.c
@@ -1357,6 +1357,9 @@ static void ip_vs_conn_expire(struct timer_list *t)
struct ip_vs_conn *cp = timer_container_of(cp, t2, timer);
struct netns_ipvs *ipvs = cp->ipvs;

+ rcu_read_lock();
+
+repeat:
/*
* do I control anybody?
*/
@@ -1366,19 +1369,20 @@ static void ip_vs_conn_expire(struct timer_list *t)
/* Unlink conn if not referenced anymore */
if (likely(ip_vs_conn_unlink(cp, my_cb))) {
struct ip_vs_conn *ct = cp->control;
+ bool next = false;

/* does anybody control me? */
if (ct) {
- rcu_read_lock();
ip_vs_control_del(cp);
/* Drop CTL or non-assured TPL if not used anymore */
if (!cp->timeout && !atomic_read(&ct->n_control) &&
(!(ct->flags & IP_VS_CONN_F_TEMPLATE) ||
!(ct->state & IP_VS_CTPL_S_ASSURED))) {
IP_VS_DBG(4, "drop controlling connection\n");
- ip_vs_conn_del(ct);
+ if (ct->control)
+ ct->timeout = 0;
+ next = true;
}
- rcu_read_unlock();
}

if ((cp->flags & IP_VS_CONN_F_NFCT) &&
@@ -1405,7 +1409,12 @@ static void ip_vs_conn_expire(struct timer_list *t)
else
call_rcu(&cp->rcu_head, ip_vs_conn_rcu_free);
atomic_dec(&ipvs->conn_count);
- return;
+ if (next) {
+ cp = ct;
+ my_cb = false;
+ goto repeat;
+ }
+ goto out;
}

expire_later:
@@ -1422,6 +1431,9 @@ static void ip_vs_conn_expire(struct timer_list *t)

__ip_vs_conn_put_timer(cp);
}
+
+out:
+ rcu_read_unlock();
}

/* Modify timer, so that it expires as soon as possible.
--
2.43.0