Hi Yizhou,

On Mon, 13 Jul 2026, Yizhou Zhao wrote:

> 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: [email protected]
> Reported-by: Yizhou Zhao <[email protected]>
> Reported-by: Yuxiang Yang <[email protected]>
> Reported-by: Ao Wang <[email protected]>
> Reported-by: Xuewei Feng <[email protected]>
> Reported-by: Qi Li <[email protected]>
> Reported-by: Ke Xu <[email protected]>
> Assisted-by: Claude-Code:GLM-5.2
> Suggested-by: Julian Anastasov <[email protected]>
> Signed-off-by: Yizhou Zhao <[email protected]>
> ---
>  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 */

> 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;
        }

>               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;

>  }
>  
>  /*
> @@ -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 <[email protected]>


Reply via email to