Re: [PATCH nf v3 2/2] ipvs: use bitops for destination overload state
From: Julian Anastasov <ja@ssi.bg>
Date: 2026-07-14 11:17:03
Also in:
lkml, lvs-devel, netfilter-devel, stable
Hi Yizhou, On Mon, 13 Jul 2026, Yizhou Zhao wrote:
quoted hunk ↗ jump to hunk
IPVS destination schedulers read the overload state from packet processing paths, while connection accounting and destination updates can change it concurrently. IP_VS_DEST_F_OVERLOAD currently shares dest->flags with IP_VS_DEST_F_AVAILABLE, so plain read-modify-write operations on the two independent states can race and lose either update. KCSAN reports the race with the SH scheduler and an upper connection threshold configured: BUG: KCSAN: data-race in __ip_vs_update_dest / ip_vs_sh_schedule IP_VS_DEST_F_AVAILABLE is changed under service_mutex. Keep it in the existing flags word, but move the overload state to a separate unsigned long and access it with bitops. Use test_bit() in scheduler paths and set_bit()/clear_bit() in ip_vs_dest_update_overload(). This serializes the overload bit accesses and prevents updates to the available and overload states from clobbering each other. The destination flags are not exposed by the IPVS sockopt or netlink interfaces, so move their definitions out of the UAPI header. Place the new overload word next to weight, which keeps the existing flags, conn_flags and weight offsets unchanged. On x86-64 this grows struct ip_vs_dest from 472 to 480 bytes. test_bit() does not add reader-side ordering. Schedulers can still observe stale destination state, as they could before this change; this does not provide a fresh cross-field snapshot. Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2") Cc: stable@vger.kernel.org Reported-by: Yizhou Zhao <redacted> Reported-by: Yuxiang Yang <redacted> Reported-by: Ao Wang <redacted> Reported-by: Xuewei Feng <redacted> Reported-by: Qi Li <redacted> Reported-by: Ke Xu <redacted> Assisted-by: Claude-Code:GLM-5.2 Suggested-by: Julian Anastasov <ja@ssi.bg> Signed-off-by: Yizhou Zhao <redacted> --- include/net/ip_vs.h | 8 ++++++++ include/uapi/linux/ip_vs.h | 6 ------ net/netfilter/ipvs/ip_vs_conn.c | 7 ++++--- net/netfilter/ipvs/ip_vs_dh.c | 4 ++-- net/netfilter/ipvs/ip_vs_fo.c | 2 +- net/netfilter/ipvs/ip_vs_lblc.c | 4 ++-- net/netfilter/ipvs/ip_vs_lblcr.c | 8 ++++---- net/netfilter/ipvs/ip_vs_lc.c | 2 +- net/netfilter/ipvs/ip_vs_mh.c | 2 +- net/netfilter/ipvs/ip_vs_nq.c | 2 +- net/netfilter/ipvs/ip_vs_ovf.c | 2 +- net/netfilter/ipvs/ip_vs_rr.c | 2 +- net/netfilter/ipvs/ip_vs_sed.c | 4 ++-- net/netfilter/ipvs/ip_vs_sh.c | 2 +- net/netfilter/ipvs/ip_vs_twos.c | 4 ++-- net/netfilter/ipvs/ip_vs_wlc.c | 4 ++-- net/netfilter/ipvs/ip_vs_wrr.c | 2 +- 17 files changed, 34 insertions(+), 31 deletions(-)diff --git a/include/net/ip_vs.h b/include/net/ip_vs.h index 3fc864a320fb..5e8e55f82b04 100644 --- a/include/net/ip_vs.h +++ b/include/net/ip_vs.h@@ -36,6 +36,13 @@ #define IP_VS_HDR_INVERSE 1 #define IP_VS_HDR_ICMP 2 +/* Destination Server Flags */ +#define IP_VS_DEST_F_AVAILABLE 0x0001 /* server is available */ + +enum { + IP_VS_DEST_FL_OVERLOAD, +}; + /* conn_tab limits (as per Kconfig) */ #define IP_VS_CONN_TAB_MIN_BITS 8 #if BITS_PER_LONG > 32@@ -976,6 +983,7 @@ struct ip_vs_dest { volatile unsigned int flags; /* dest status flags */
Sashiko has some comments that we should fix somehow: https://sashiko.dev/#/patchset/cover.1783931964.git.zhaoyz24%40mails.tsinghua.edu.cn One option is IP_VS_DEST_F_AVAILABLE to become IP_VS_DEST_CF_AVAILABLE (CF=Config Flag)
atomic_t conn_flags; /* flags to copy to conn */ atomic_t weight; /* server weight */ + unsigned long flags2; /* dest status flags */
unsigned long cfg_flags; We then put IP_VS_DEST_CF_AVAILABLE in this new cache line that most of the schedulers will not read until dest is selected. DH even should not check the IP_VS_DEST_F_AVAILABLE flag, only lblc/lblcr should use this flag. We can preserve IP_VS_DEST_F_OVERLOAD in 'flags', even we may not need to use bitops if we start to use spin_lock_bh(&dest->dst_lock), as this lock is already present in the dest structure. See below...
atomic_t last_weight; /* server latest weight */ __u16 tun_type; /* tunnel type */ __be16 tun_port; /* tunnel port */
quoted hunk ↗ jump to hunk
diff --git a/net/netfilter/ipvs/ip_vs_conn.c b/net/netfilter/ipvs/ip_vs_conn.c index fa3fbd597f3f..2591f4e143f8 100644 --- a/net/netfilter/ipvs/ip_vs_conn.c +++ b/net/netfilter/ipvs/ip_vs_conn.c@@ -1006,7 +1006,7 @@ __always_inline void ip_vs_dest_update_overload(struct ip_vs_dest *dest)
We can add new arg 'bool locked'. Also, we will return false if caller should retry under lock. It will happen when we change IP_VS_DEST_F_OVERLOAD and require its changes to be synchronized with the thresholds and the number of connections.
goto unset;
conns = ip_vs_dest_totalconns(dest);
if (conns >= u) {
- dest->flags |= IP_VS_DEST_F_OVERLOAD;
+ set_bit(IP_VS_DEST_FL_OVERLOAD, &dest->flags2);
if (conns >= u) {
if (!locked)
return false;
dest->flags |= IP_VS_DEST_F_OVERLOAD;
return true;
}
quoted hunk ↗ jump to hunk
return; } /* Low threshold defaults to 75% of upper threshold */@@ -1015,7 +1015,8 @@ __always_inline void ip_vs_dest_update_overload(struct ip_vs_dest *dest) return; unset: - dest->flags &= ~IP_VS_DEST_F_OVERLOAD; + if (test_bit(IP_VS_DEST_FL_OVERLOAD, &dest->flags2)) + clear_bit(IP_VS_DEST_FL_OVERLOAD, &dest->flags2);
if (dest->flags & IP_VS_DEST_F_OVERLOAD) {
if (!locked)
return false;
dest->flags &= ~IP_VS_DEST_F_OVERLOAD;
}
return true;
quoted hunk ↗ jump to hunk
} /*@@ -1174,7 +1175,7 @@ static inline void ip_vs_unbind_dest(struct ip_vs_conn *cp) atomic_dec(&dest->persistconns); } - if (dest->flags & IP_VS_DEST_F_OVERLOAD) + if (test_bit(IP_VS_DEST_FL_OVERLOAD, &dest->flags2)) ip_vs_dest_update_overload(dest);
if (dest->flags & IP_VS_DEST_F_OVERLOAD) {
if (!ip_vs_dest_update_overload(dest, false)) {
spin_lock_bh(&dest->dst_lock);
ip_vs_dest_update_overload(dest, true);
spin_unlock_bh(&dest->dst_lock);
}
}
In __ip_vs_update_dest() we will always use lock:
spin_lock_bh(&dest->dst_lock);
WRITE_ONCE(dest->u_threshold, udest->u_threshold);
WRITE_ONCE(dest->l_threshold, udest->l_threshold);
ip_vs_dest_update_overload(dest, true);
spin_unlock_bh(&dest->dst_lock);
The goal is to avoid the lock for the common case
when flag does not change. What do you think?
Regards
--
Julian Anastasov [off-list ref]