From 48d522b65fb0ce2bd54f8f5838d77b48afcf1f3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Jean?= Date: Sat, 22 Aug 2026 21:40:13 +0000 Subject: [PATCH 01/10] netfilter: nf_conntrack: avoid truncating IPv6 nexthdr offset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit find_prev_fhdr() returns the offset of the Next Header field before an IPv6 Fragment header through an int pointer, but stores the offset in a u8 local. An extension header chain can put this field beyond byte 255, causing the offset to wrap before it is saved in the fragment queue. Reassembly later uses the wrapped value to update the preceding Next Header field. This overwrites another byte in the IPv6 header chain and can make conntrack parse a different transport tuple from the one used by IPv6 local delivery. A two-fragment packet can, for example, be recorded as UDP destination port 53 while its payload is delivered to UDP port 9999. Use int for the working offset, matching the output argument and the other offset variables in this path. Fixes: 9fb9cbb1082d ("[NETFILTER]: Add nf_conntrack subsystem.") Assisted-by: Codex:gpt-5 Signed-off-by: Jérémy Jean Signed-off-by: Pablo Neira Ayuso --- net/ipv6/netfilter/nf_conntrack_reasm.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/net/ipv6/netfilter/nf_conntrack_reasm.c b/net/ipv6/netfilter/nf_conntrack_reasm.c index 599c49bf0a0a..be72c4346f8b 100644 --- a/net/ipv6/netfilter/nf_conntrack_reasm.c +++ b/net/ipv6/netfilter/nf_conntrack_reasm.c @@ -398,7 +398,7 @@ find_prev_fhdr(struct sk_buff *skb, u8 *prevhdrp, int *prevhoff, int *fhoff) { u8 nexthdr = ipv6_hdr(skb)->nexthdr; const int netoff = skb_network_offset(skb); - u8 prev_nhoff = netoff + offsetof(struct ipv6hdr, nexthdr); + int prev_nhoff = netoff + offsetof(struct ipv6hdr, nexthdr); int start = netoff + sizeof(struct ipv6hdr); int len = skb->len - start; u8 prevhdr = NEXTHDR_IPV6; From c2226f0c7fe90b67732e5ae36dc5c3cc01ddf888 Mon Sep 17 00:00:00 2001 From: Florian Westphal Date: Sat, 5 Sep 2026 07:01:50 +0200 Subject: [PATCH 02/10] netfilter: nft_payload: restrict checksum offsets to known values We need to prevent userspace from corrupting e.g. tcp->doff, because many locations in conntrack and conntrack helpers rely on nf_conntrack_in() having validated the packet headers. nft_payload allows to alter headers later which invalidates this assumption. The 'Fixes' commit restricts writes to safe fields, but there is another side channel: the checksum location. Restrict this too. Reported via sashiko/gemini. Fixes: 112e447d17f7 ("netfilter: validate L4 headers after userspace packet writes") Assisted-by: Claude:claude-opus-4-6 Signed-off-by: Florian Westphal Signed-off-by: Pablo Neira Ayuso --- net/netfilter/nft_payload.c | 40 ++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/net/netfilter/nft_payload.c b/net/netfilter/nft_payload.c index e315d35f73d4..5b76af68b704 100644 --- a/net/netfilter/nft_payload.c +++ b/net/netfilter/nft_payload.c @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -1008,11 +1009,14 @@ static bool nft_payload_validate_inet_csum_offset(const struct nft_ctx *ctx, if (priv->csum_flags) /* makes no sense, asks for "re-update" of L4 checksum */ return false; - /* no further check here; offset can't be negative so bogus - * offsets can corrupt L4 or payload but not l3 headers. - * We already allow arbitrary l4/inner payload writes. - */ - return true; + /* Validate csum_offset is one of the supported transport header checksums */ + if (priv->csum_offset == offsetof(struct tcphdr, check) || + priv->csum_offset == offsetof(struct udphdr, check) || + priv->csum_offset == offsetof(struct icmphdr, checksum) || + priv->csum_offset == offsetof(struct icmp6hdr, icmp6_cksum)) + return true; + + return false; case NFT_PAYLOAD_INNER_HEADER: return true; case NFT_PAYLOAD_TUN_HEADER: @@ -1046,6 +1050,27 @@ static bool nft_payload_csum_nh_write_ok(const struct nft_payload_set *priv, return false; } +static bool nft_payload_csum_th_write_ok(const struct nft_payload_set *priv, + const struct nft_pktinfo *pkt) +{ + if (!(pkt->flags & NFT_PKTINFO_L4PROTO)) + return false; + + switch (pkt->tprot) { + case IPPROTO_TCP: + return priv->csum_offset == offsetof(struct tcphdr, check); + case IPPROTO_UDP: + case IPPROTO_UDPLITE: + return priv->csum_offset == offsetof(struct udphdr, check); + case IPPROTO_ICMPV6: + return priv->csum_offset == offsetof(struct icmp6hdr, icmp6_cksum); + case IPPROTO_ICMP: + return priv->csum_offset == offsetof(struct icmphdr, checksum); + } + + return false; +} + static bool nft_payload_csum_write_ok(const struct nft_pktinfo *pkt, const struct nft_payload_set *priv) { @@ -1055,9 +1080,10 @@ static bool nft_payload_csum_write_ok(const struct nft_pktinfo *pkt, case NFT_PAYLOAD_NETWORK_HEADER: return nft_payload_csum_nh_write_ok(priv, pkt); case NFT_PAYLOAD_TRANSPORT_HEADER: + return nft_payload_csum_th_write_ok(priv, pkt); case NFT_PAYLOAD_INNER_HEADER: - /* neither offsets are validated, offsets cannot be - * negative so real l3 headers cannot be mangled. + /* offset is not validated, offset cannot be + * negative so real l3/l4 headers cannot be mangled. */ return true; case NFT_PAYLOAD_TUN_HEADER: From 3ba20137287f4f34ddbf0afb53c5d1ba03b6a8c2 Mon Sep 17 00:00:00 2001 From: Julian Anastasov Date: Mon, 7 Sep 2026 21:34:53 +0300 Subject: [PATCH 03/10] ipvs: fix more races around the overload flag Sashiko warns that we still have races when the dest overload thresholds are updated. If we narrow the range between thresholds the lockless readers (ip_vs_bind_dest and ip_vs_unbind_dest) may use old threshold values and as result to skip the required flag change. To solve the problem ip_vs_bind_dest() and ip_vs_unbind_dest() will have the chance as before to change the overload state not only at the exact threshold positions but at any conn count below the lower and above the upper thresholds. To make it work without entering the locked region for every connection, we introduce l_threshold_val. Together with u_threshold_val we will switch between two states: 1. not overloaded: where l_threshold_val is set to 0 to close the door for more ip_vs_unbind_dest() calls to clear the overload state. 2. overloaded: where u_threshold_val is set to INT_MAX to close the door for more ip_vs_bind_dest() calls to set the overload state. If a race occurs, multiple CPUs can repeat the state change but this should be for short time window. With the introduction of l_threshold_val the IP_VS_DEST_F_OVERLOAD flag and the 'flags' field are not needed anymore. We will use the ip_vs_dest_is_overloaded() helper for all places that read the overload state. Do not check IP_VS_DEST_CF_AVAILABLE in ip_vs_dh.c because we do not hold reference to dest after it is removed and rely on RCU to access the available dests. To reduce the cacheline accesses while selecting destination, keep the mostly used fields (weight and l_threshold_val) at the first read-mostly cacheline together with the hash nodes. Fixes: 8f843441c4e7 ("ipvs: properly update the overload flag on dest edit") Link: https://sashiko.dev/#/patchset/20260810190621.894119-1-pablo%40netfilter.org Signed-off-by: Julian Anastasov Signed-off-by: Pablo Neira Ayuso --- include/net/ip_vs.h | 50 ++++++++++++++++++++------------ net/netfilter/ipvs/ip_vs_conn.c | 8 ++--- net/netfilter/ipvs/ip_vs_ctl.c | 38 ++++++++++++------------ net/netfilter/ipvs/ip_vs_dh.c | 16 ++-------- 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, 76 insertions(+), 76 deletions(-) diff --git a/include/net/ip_vs.h b/include/net/ip_vs.h index 32fde731bceb..9a83bff32569 100644 --- a/include/net/ip_vs.h +++ b/include/net/ip_vs.h @@ -36,9 +36,6 @@ #define IP_VS_HDR_INVERSE 1 #define IP_VS_HDR_ICMP 2 -/* Destination Server Flags */ -#define IP_VS_DEST_F_OVERLOAD 0x0002 /* server is overloaded */ - /* Destination Server Config Flags */ #define IP_VS_DEST_CF_AVAILABLE 0x0001 /* server is available */ @@ -973,33 +970,44 @@ struct ip_vs_dest_dst { * and so on. */ struct ip_vs_dest { + /* Cacheline for hash table nodes - read-mostly */ + struct list_head n_list; /* for the dests in the service */ struct hlist_node d_list; /* for table with all the dests */ - u16 af; /* address family */ - __be16 port; /* port number of the server */ - union nf_inet_addr addr; /* IP address of the server */ - volatile unsigned int flags; /* dest status flags */ - atomic_t conn_flags; /* flags to copy to conn */ atomic_t weight; /* server weight */ - unsigned long cflags; /* config flags */ atomic_t last_weight; /* server latest weight */ + + /* connection thresholds */ + u32 l_threshold_val;/* used lower threshold */ + u32 u_threshold_val;/* used upper threshold */ + /* 32/48 */ + u32 l_threshold; /* lower threshold */ + u32 u_threshold; /* upper threshold */ + + unsigned long cflags; /* config flags */ + + /* 44/64 */ + atomic_t conn_flags; /* flags to copy to conn */ + __u16 tun_type; /* tunnel type */ __be16 tun_port; /* tunnel port */ __u16 tun_flags; /* tunnel flags */ + u16 af; /* address family */ + __be16 port; /* port number of the server */ + /* 60/80 */ + union nf_inet_addr addr; /* IP address of the server */ + + /* connection counters */ + atomic_t totalconns; /* total connections */ + atomic_t activeconns; /* active connections */ + atomic_t persistconns; /* persistent connections */ + refcount_t refcnt; /* reference counter */ struct ip_vs_stats stats; /* statistics */ unsigned long idle_start; /* start time, jiffies */ - /* connection counters and thresholds */ - atomic_t activeconns; /* active connections */ - atomic_t totalconns; /* total connections */ - atomic_t persistconns; /* persistent connections */ - __u32 u_threshold; /* upper threshold */ - __u32 l_threshold; /* lower threshold */ - __u32 l_threshold_val;/* used lower threshold */ - /* for destination cache */ spinlock_t dst_lock; /* lock of dst_cache */ struct ip_vs_dest_dst __rcu *dest_dst; /* cached dst info */ @@ -1915,7 +1923,13 @@ static inline void ip_vs_dest_put_and_free(struct ip_vs_dest *dest) kfree(dest); } -void ip_vs_dest_update_overload(struct ip_vs_dest *dest, int mode); +void ip_vs_dest_update_overload(struct ip_vs_dest *dest, bool overload); + +/* Check if dest is in overloaded state */ +static inline bool ip_vs_dest_is_overloaded(const struct ip_vs_dest *dest) +{ + return READ_ONCE(dest->l_threshold_val); +} /* IPVS sync daemon data and function prototypes * (from ip_vs_sync.c) diff --git a/net/netfilter/ipvs/ip_vs_conn.c b/net/netfilter/ipvs/ip_vs_conn.c index cb009208826f..94f25fb7d87e 100644 --- a/net/netfilter/ipvs/ip_vs_conn.c +++ b/net/netfilter/ipvs/ip_vs_conn.c @@ -1153,8 +1153,8 @@ ip_vs_bind_dest(struct ip_vs_conn *cp, struct ip_vs_dest *dest) if (!(flags & IP_VS_CONN_F_INACTIVE)) atomic_inc(&dest->activeconns); tc = atomic_inc_return(&dest->totalconns); - if (tc == READ_ONCE(dest->u_threshold)) - ip_vs_dest_update_overload(dest, 1); + if (tc >= READ_ONCE(dest->u_threshold_val)) + ip_vs_dest_update_overload(dest, true); } else { /* It is a persistent connection/template, so increase the persistent connection counter */ @@ -1246,8 +1246,8 @@ static inline void ip_vs_unbind_dest(struct ip_vs_conn *cp) if (!(cp->flags & IP_VS_CONN_F_INACTIVE)) atomic_dec(&dest->activeconns); tc = atomic_fetch_dec(&dest->totalconns); - if (tc == READ_ONCE(dest->l_threshold_val)) - ip_vs_dest_update_overload(dest, -1); + if (tc <= READ_ONCE(dest->l_threshold_val)) + ip_vs_dest_update_overload(dest, false); } else { /* It is a persistent connection/template, so decrease the persistent connection counter */ diff --git a/net/netfilter/ipvs/ip_vs_ctl.c b/net/netfilter/ipvs/ip_vs_ctl.c index 4c1c739446b7..3003547dcabe 100644 --- a/net/netfilter/ipvs/ip_vs_ctl.c +++ b/net/netfilter/ipvs/ip_vs_ctl.c @@ -1309,32 +1309,32 @@ void ip_vs_stats_free(struct ip_vs_stats *stats) * - conns reach u_threshold and exceed it: set the flag * - conns go below l_threshold (or 75% of u_threshold): clear the flag */ -static void __ip_vs_dest_update_overload(struct ip_vs_dest *dest, int mode) +static void __ip_vs_dest_update_overload(struct ip_vs_dest *dest, bool overload) { int conns; u32 l, u; lockdep_assert_held(&dest->dst_lock); u = READ_ONCE(dest->u_threshold); - if (!u) - goto unset; - l = READ_ONCE(dest->l_threshold_val); - conns = atomic_read(&dest->totalconns); - if (conns >= (mode > 0 ? l : u)) { - dest->flags |= IP_VS_DEST_F_OVERLOAD; - return; + if (u) { + /* Low threshold defaults to 75% of upper threshold */ + l = READ_ONCE(dest->l_threshold) ? : (u - (u >> 2)); + conns = atomic_read(&dest->totalconns); + if (conns >= (overload ? l : u)) { + WRITE_ONCE(dest->u_threshold_val, INT_MAX); + WRITE_ONCE(dest->l_threshold_val, l); + return; + } } - if (conns >= (mode < 0 ? u : l)) - return; -unset: - dest->flags &= ~IP_VS_DEST_F_OVERLOAD; + WRITE_ONCE(dest->u_threshold_val, u ? : INT_MAX); + WRITE_ONCE(dest->l_threshold_val, 0); } -void ip_vs_dest_update_overload(struct ip_vs_dest *dest, int mode) +void ip_vs_dest_update_overload(struct ip_vs_dest *dest, bool overload) { spin_lock_bh(&dest->dst_lock); - __ip_vs_dest_update_overload(dest, mode); + __ip_vs_dest_update_overload(dest, overload); spin_unlock_bh(&dest->dst_lock); } @@ -1406,15 +1406,13 @@ __ip_vs_update_dest(struct ip_vs_service *svc, struct ip_vs_dest *dest, if (READ_ONCE(dest->u_threshold) != udest->u_threshold || READ_ONCE(dest->l_threshold) != udest->l_threshold) { + bool overload; + spin_lock_bh(&dest->dst_lock); WRITE_ONCE(dest->u_threshold, udest->u_threshold); WRITE_ONCE(dest->l_threshold, udest->l_threshold); - /* Low threshold defaults to 75% of upper threshold */ - WRITE_ONCE(dest->l_threshold_val, - udest->l_threshold ? : - (udest->u_threshold - - (udest->u_threshold >> 2))); - __ip_vs_dest_update_overload(dest, 0); + overload = ip_vs_dest_is_overloaded(dest); + __ip_vs_dest_update_overload(dest, overload); spin_unlock_bh(&dest->dst_lock); } diff --git a/net/netfilter/ipvs/ip_vs_dh.c b/net/netfilter/ipvs/ip_vs_dh.c index 43abed7a26a6..95e52950adbb 100644 --- a/net/netfilter/ipvs/ip_vs_dh.c +++ b/net/netfilter/ipvs/ip_vs_dh.c @@ -195,16 +195,6 @@ static int ip_vs_dh_dest_changed(struct ip_vs_service *svc, } -/* - * If the dest flags is set with IP_VS_DEST_F_OVERLOAD, - * consider that the server is overloaded here. - */ -static inline int is_overloaded(struct ip_vs_dest *dest) -{ - return dest->flags & IP_VS_DEST_F_OVERLOAD; -} - - /* * Destination hashing scheduling */ @@ -219,10 +209,8 @@ ip_vs_dh_schedule(struct ip_vs_service *svc, const struct sk_buff *skb, s = (struct ip_vs_dh_state *) svc->sched_data; dest = ip_vs_dh_get(svc->af, s, &iph->daddr); - if (!dest || - !(dest->cflags & IP_VS_DEST_CF_AVAILABLE) - || atomic_read(&dest->weight) <= 0 - || is_overloaded(dest)) { + if (!dest || atomic_read(&dest->weight) <= 0 || + ip_vs_dest_is_overloaded(dest)) { ip_vs_scheduler_err(svc, "no destination available"); return NULL; } diff --git a/net/netfilter/ipvs/ip_vs_fo.c b/net/netfilter/ipvs/ip_vs_fo.c index d657b47c6511..e07fa33f6d52 100644 --- a/net/netfilter/ipvs/ip_vs_fo.c +++ b/net/netfilter/ipvs/ip_vs_fo.c @@ -29,7 +29,7 @@ ip_vs_fo_schedule(struct ip_vs_service *svc, const struct sk_buff *skb, * Find virtual server with highest weight and send it traffic */ list_for_each_entry_rcu(dest, &svc->destinations, n_list) { - if (!(dest->flags & IP_VS_DEST_F_OVERLOAD) && + if (!ip_vs_dest_is_overloaded(dest) && atomic_read(&dest->weight) > hw) { hweight = dest; hw = atomic_read(&dest->weight); diff --git a/net/netfilter/ipvs/ip_vs_lblc.c b/net/netfilter/ipvs/ip_vs_lblc.c index 55af77a56929..e1111c3b2721 100644 --- a/net/netfilter/ipvs/ip_vs_lblc.c +++ b/net/netfilter/ipvs/ip_vs_lblc.c @@ -418,7 +418,7 @@ __ip_vs_lblc_schedule(struct ip_vs_service *svc) * new connection. */ list_for_each_entry_rcu(dest, &svc->destinations, n_list) { - if (dest->flags & IP_VS_DEST_F_OVERLOAD) + if (ip_vs_dest_is_overloaded(dest)) continue; if (atomic_read(&dest->weight) > 0) { least = dest; @@ -433,7 +433,7 @@ __ip_vs_lblc_schedule(struct ip_vs_service *svc) */ nextstage: list_for_each_entry_continue_rcu(dest, &svc->destinations, n_list) { - if (dest->flags & IP_VS_DEST_F_OVERLOAD) + if (ip_vs_dest_is_overloaded(dest)) continue; doh = ip_vs_dest_conn_overhead(dest); diff --git a/net/netfilter/ipvs/ip_vs_lblcr.c b/net/netfilter/ipvs/ip_vs_lblcr.c index 858393b1d2d1..dfb5bf83750d 100644 --- a/net/netfilter/ipvs/ip_vs_lblcr.c +++ b/net/netfilter/ipvs/ip_vs_lblcr.c @@ -166,7 +166,7 @@ static inline struct ip_vs_dest *ip_vs_dest_set_min(struct ip_vs_dest_set *set) /* select the first destination server, whose weight > 0 */ list_for_each_entry_rcu(e, &set->list, list) { least = e->dest; - if (least->flags & IP_VS_DEST_F_OVERLOAD) + if (ip_vs_dest_is_overloaded(least)) continue; if ((atomic_read(&least->weight) > 0) && @@ -181,7 +181,7 @@ static inline struct ip_vs_dest *ip_vs_dest_set_min(struct ip_vs_dest_set *set) nextstage: list_for_each_entry_continue_rcu(e, &set->list, list) { dest = e->dest; - if (dest->flags & IP_VS_DEST_F_OVERLOAD) + if (ip_vs_dest_is_overloaded(dest)) continue; doh = ip_vs_dest_conn_overhead(dest); @@ -580,7 +580,7 @@ __ip_vs_lblcr_schedule(struct ip_vs_service *svc) * new connection. */ list_for_each_entry_rcu(dest, &svc->destinations, n_list) { - if (dest->flags & IP_VS_DEST_F_OVERLOAD) + if (ip_vs_dest_is_overloaded(dest)) continue; if (atomic_read(&dest->weight) > 0) { @@ -596,7 +596,7 @@ __ip_vs_lblcr_schedule(struct ip_vs_service *svc) */ nextstage: list_for_each_entry_continue_rcu(dest, &svc->destinations, n_list) { - if (dest->flags & IP_VS_DEST_F_OVERLOAD) + if (ip_vs_dest_is_overloaded(dest)) continue; doh = ip_vs_dest_conn_overhead(dest); diff --git a/net/netfilter/ipvs/ip_vs_lc.c b/net/netfilter/ipvs/ip_vs_lc.c index 18b976a7c4d2..9002f491bff6 100644 --- a/net/netfilter/ipvs/ip_vs_lc.c +++ b/net/netfilter/ipvs/ip_vs_lc.c @@ -38,7 +38,7 @@ ip_vs_lc_schedule(struct ip_vs_service *svc, const struct sk_buff *skb, */ list_for_each_entry_rcu(dest, &svc->destinations, n_list) { - if ((dest->flags & IP_VS_DEST_F_OVERLOAD) || + if (ip_vs_dest_is_overloaded(dest) || atomic_read(&dest->weight) == 0) continue; doh = ip_vs_dest_conn_overhead(dest); diff --git a/net/netfilter/ipvs/ip_vs_mh.c b/net/netfilter/ipvs/ip_vs_mh.c index 020863047562..d70b23aec5fe 100644 --- a/net/netfilter/ipvs/ip_vs_mh.c +++ b/net/netfilter/ipvs/ip_vs_mh.c @@ -80,7 +80,7 @@ static inline void generate_hash_secret(hsiphash_key_t *hash1, static inline bool is_unavailable(struct ip_vs_dest *dest) { return atomic_read(&dest->weight) <= 0 || - dest->flags & IP_VS_DEST_F_OVERLOAD; + ip_vs_dest_is_overloaded(dest); } /* Returns hash value for IPVS MH entry */ diff --git a/net/netfilter/ipvs/ip_vs_nq.c b/net/netfilter/ipvs/ip_vs_nq.c index ada158c610ce..7cc28902086e 100644 --- a/net/netfilter/ipvs/ip_vs_nq.c +++ b/net/netfilter/ipvs/ip_vs_nq.c @@ -72,7 +72,7 @@ ip_vs_nq_schedule(struct ip_vs_service *svc, const struct sk_buff *skb, list_for_each_entry_rcu(dest, &svc->destinations, n_list) { - if (dest->flags & IP_VS_DEST_F_OVERLOAD || + if (ip_vs_dest_is_overloaded(dest) || !atomic_read(&dest->weight)) continue; diff --git a/net/netfilter/ipvs/ip_vs_ovf.c b/net/netfilter/ipvs/ip_vs_ovf.c index c5c67df80a0b..eaa3a7ae6efa 100644 --- a/net/netfilter/ipvs/ip_vs_ovf.c +++ b/net/netfilter/ipvs/ip_vs_ovf.c @@ -33,7 +33,7 @@ ip_vs_ovf_schedule(struct ip_vs_service *svc, const struct sk_buff *skb, */ list_for_each_entry_rcu(dest, &svc->destinations, n_list) { w = atomic_read(&dest->weight); - if ((dest->flags & IP_VS_DEST_F_OVERLOAD) || + if (ip_vs_dest_is_overloaded(dest) || atomic_read(&dest->activeconns) > w || w == 0) continue; diff --git a/net/netfilter/ipvs/ip_vs_rr.c b/net/netfilter/ipvs/ip_vs_rr.c index 4125ee561cdc..bd17cdd092f5 100644 --- a/net/netfilter/ipvs/ip_vs_rr.c +++ b/net/netfilter/ipvs/ip_vs_rr.c @@ -66,7 +66,7 @@ ip_vs_rr_schedule(struct ip_vs_service *svc, const struct sk_buff *skb, list_for_each_entry_continue_rcu(dest, &svc->destinations, n_list) { - if (!(dest->flags & IP_VS_DEST_F_OVERLOAD) && + if (!ip_vs_dest_is_overloaded(dest) && atomic_read(&dest->weight) > 0) /* HIT */ goto out; diff --git a/net/netfilter/ipvs/ip_vs_sed.c b/net/netfilter/ipvs/ip_vs_sed.c index 245a323c84cd..7925f4d28fce 100644 --- a/net/netfilter/ipvs/ip_vs_sed.c +++ b/net/netfilter/ipvs/ip_vs_sed.c @@ -75,7 +75,7 @@ ip_vs_sed_schedule(struct ip_vs_service *svc, const struct sk_buff *skb, */ list_for_each_entry_rcu(dest, &svc->destinations, n_list) { - if (!(dest->flags & IP_VS_DEST_F_OVERLOAD) && + if (!ip_vs_dest_is_overloaded(dest) && atomic_read(&dest->weight) > 0) { least = dest; loh = ip_vs_sed_dest_overhead(least); @@ -90,7 +90,7 @@ ip_vs_sed_schedule(struct ip_vs_service *svc, const struct sk_buff *skb, */ nextstage: list_for_each_entry_continue_rcu(dest, &svc->destinations, n_list) { - if (dest->flags & IP_VS_DEST_F_OVERLOAD) + if (ip_vs_dest_is_overloaded(dest)) continue; doh = ip_vs_sed_dest_overhead(dest); if ((__s64)loh * atomic_read(&dest->weight) > diff --git a/net/netfilter/ipvs/ip_vs_sh.c b/net/netfilter/ipvs/ip_vs_sh.c index cd67066e3b26..c24dd5cb7e47 100644 --- a/net/netfilter/ipvs/ip_vs_sh.c +++ b/net/netfilter/ipvs/ip_vs_sh.c @@ -73,7 +73,7 @@ struct ip_vs_sh_state { static inline bool is_unavailable(struct ip_vs_dest *dest) { return atomic_read(&dest->weight) <= 0 || - dest->flags & IP_VS_DEST_F_OVERLOAD; + ip_vs_dest_is_overloaded(dest); } /* diff --git a/net/netfilter/ipvs/ip_vs_twos.c b/net/netfilter/ipvs/ip_vs_twos.c index dbb7f5fd4688..86186122a1f0 100644 --- a/net/netfilter/ipvs/ip_vs_twos.c +++ b/net/netfilter/ipvs/ip_vs_twos.c @@ -52,7 +52,7 @@ static struct ip_vs_dest *ip_vs_twos_schedule(struct ip_vs_service *svc, /* Generate a random weight between [0,sum of all weights) */ list_for_each_entry_rcu(dest, &svc->destinations, n_list) { - if (!(dest->flags & IP_VS_DEST_F_OVERLOAD)) { + if (!ip_vs_dest_is_overloaded(dest)) { weight = atomic_read(&dest->weight); if (weight > 0) { total_weight += weight; @@ -75,7 +75,7 @@ static struct ip_vs_dest *ip_vs_twos_schedule(struct ip_vs_service *svc, /* Pick two weighted servers */ list_for_each_entry_rcu(dest, &svc->destinations, n_list) { - if (dest->flags & IP_VS_DEST_F_OVERLOAD) + if (ip_vs_dest_is_overloaded(dest)) continue; weight = atomic_read(&dest->weight); diff --git a/net/netfilter/ipvs/ip_vs_wlc.c b/net/netfilter/ipvs/ip_vs_wlc.c index 9da445ca09a1..08e9e22f72bf 100644 --- a/net/netfilter/ipvs/ip_vs_wlc.c +++ b/net/netfilter/ipvs/ip_vs_wlc.c @@ -47,7 +47,7 @@ ip_vs_wlc_schedule(struct ip_vs_service *svc, const struct sk_buff *skb, */ list_for_each_entry_rcu(dest, &svc->destinations, n_list) { - if (!(dest->flags & IP_VS_DEST_F_OVERLOAD) && + if (!ip_vs_dest_is_overloaded(dest) && atomic_read(&dest->weight) > 0) { least = dest; loh = ip_vs_dest_conn_overhead(least); @@ -62,7 +62,7 @@ ip_vs_wlc_schedule(struct ip_vs_service *svc, const struct sk_buff *skb, */ nextstage: list_for_each_entry_continue_rcu(dest, &svc->destinations, n_list) { - if (dest->flags & IP_VS_DEST_F_OVERLOAD) + if (ip_vs_dest_is_overloaded(dest)) continue; doh = ip_vs_dest_conn_overhead(dest); if ((__s64)loh * atomic_read(&dest->weight) > diff --git a/net/netfilter/ipvs/ip_vs_wrr.c b/net/netfilter/ipvs/ip_vs_wrr.c index 2dcff1040da5..fad69bae5a22 100644 --- a/net/netfilter/ipvs/ip_vs_wrr.c +++ b/net/netfilter/ipvs/ip_vs_wrr.c @@ -176,7 +176,7 @@ ip_vs_wrr_schedule(struct ip_vs_service *svc, const struct sk_buff *skb, list_for_each_entry_continue_rcu(dest, &svc->destinations, n_list) { - if (!(dest->flags & IP_VS_DEST_F_OVERLOAD) && + if (!ip_vs_dest_is_overloaded(dest) && atomic_read(&dest->weight) >= mark->cw) goto found; if (dest == stop) From 7f1a04254a05e14845ff3d377931f941f62e41d5 Mon Sep 17 00:00:00 2001 From: Joel Granados Date: Tue, 22 Sep 2026 11:08:13 +0200 Subject: [PATCH 04/10] ipvs: skip committing sync_threshold when proc_dointvec fails Forward rc value and skip updating sysctl_sync_threshold when proc_dointvec fails. This targets a write such as "5 x" which would fail with -EINVAL but would store the first element anyway. Signed-off-by: Joel Granados Acked-by: Julian Anastasov Signed-off-by: Pablo Neira Ayuso --- net/netfilter/ipvs/ip_vs_ctl.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/net/netfilter/ipvs/ip_vs_ctl.c b/net/netfilter/ipvs/ip_vs_ctl.c index 3003547dcabe..ce9fe20d6106 100644 --- a/net/netfilter/ipvs/ip_vs_ctl.c +++ b/net/netfilter/ipvs/ip_vs_ctl.c @@ -2452,6 +2452,8 @@ proc_do_sync_threshold(const struct ctl_table *table, int write, mutex_lock(&ipvs->sync_mutex); memcpy(val, valp, sizeof(val)); rc = proc_dointvec(&tmp, write, buffer, lenp, ppos); + if (rc) + goto out; if (write) { if (val[0] < 0 || val[1] < 0 || (val[0] >= val[1] && val[1])) @@ -2459,6 +2461,7 @@ proc_do_sync_threshold(const struct ctl_table *table, int write, else memcpy(valp, val, sizeof(val)); } +out: mutex_unlock(&ipvs->sync_mutex); return rc; } From c203ecf905745d979de5df28fa657121f2e9342d Mon Sep 17 00:00:00 2001 From: Shuangfeng He Date: Fri, 2 Oct 2026 16:42:53 +0800 Subject: [PATCH 05/10] netfilter: flowtable: use the vlan id, not the full tci, in the tuple key nf_flow_tuple_encap() stores the skb vlan tag in the encap tuple id using skb_vlan_tag_get(), which returns the unmasked TCI including the 3 PCP priority bits. The flowtable entry, however, is programmed by nf_dev_path_info() with vlan_dev_vlan_id(), a plain 12-bit VID. When a NIC delivers RX vlan offload skbs with a non-zero priority (an RTL9617C EPON ONU was observed handing over vid 818 skbs with tci 0x6332, PCP 3), the runtime lookup key never matches the programmed entry, so every packet misses the flow table and software flow offload is silently defeated. Mask both parse sites to the 12-bit VID so the lookup key matches for any priority. The in-header variant has the same problem for frames received with PCP != 0 on a non-offload path. The egress side is unaffected: nf_flow_encap_push() restores tags from the programmed tuple id, which never carried priority bits. Measured on an RTL9617C ONU (PPPoE over VLAN 818 WAN, NATed 6-flow download over a 1G EPON line), before -> after: download throughput 377 Mbps -> 936 Mbps CPU0 %soft at that rate 91.5% -> 70.9% Fixes: 4cd91f7c290f ("netfilter: flowtable: add vlan support") Signed-off-by: Shuangfeng He Signed-off-by: Pablo Neira Ayuso --- net/netfilter/nf_flow_table_ip.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/net/netfilter/nf_flow_table_ip.c b/net/netfilter/nf_flow_table_ip.c index c8c29a9a1684..2dec15d402ca 100644 --- a/net/netfilter/nf_flow_table_ip.c +++ b/net/netfilter/nf_flow_table_ip.c @@ -170,14 +170,14 @@ static void nf_flow_tuple_encap(struct nf_flowtable_ctx *ctx, int i = 0; if (skb_vlan_tag_present(skb)) { - tuple->encap[i].id = skb_vlan_tag_get(skb); + tuple->encap[i].id = skb_vlan_tag_get_id(skb); tuple->encap[i].proto = skb->vlan_proto; i++; } switch (skb->protocol) { case htons(ETH_P_8021Q): veth = (struct vlan_ethhdr *)skb_mac_header(skb); - tuple->encap[i].id = ntohs(veth->h_vlan_TCI); + tuple->encap[i].id = ntohs(veth->h_vlan_TCI) & VLAN_VID_MASK; tuple->encap[i].proto = skb->protocol; offset += VLAN_HLEN; break; From 73a73d442f9986353fe284bd1b504d0313d0f153 Mon Sep 17 00:00:00 2001 From: Kyle Zeng Date: Wed, 7 Oct 2026 23:01:00 +0200 Subject: [PATCH 06/10] netfilter: nf_reject: initialize IPCB at inet ingress The inet ingress hook runs before ip_rcv_core() and ip6_rcv_core(), so neither IPCB nor IP6CB has been initialized when nft_reject_inet passes a packet to nf_send_unreach() or nf_send_unreach6(). skb->cb can therefore retain metadata from an earlier protocol layer. For IPv4, icmp_send() treats IPCB(skb)->opt as parsed IPv4 options. Stale option offsets can make __ip_options_echo() copy an attacker-controlled length into its 40-byte reply option buffer and overflow the stack. On an unpatched v7.2-rc5 kernel, the IPv4 reproducer produced: BUG: KASAN: stack-out-of-bounds in __ip_options_echo() Write of size 255 For IPv6, icmpv6_send() consumes IP6CB(skb) as parsed IPv6 metadata. It always uses iif for source selection and, when MIP6 is enabled, uses dsthao as an extension-header offset. These fields must not be stale. For NF_INET_INGRESS, clear the address-family-specific control block and restore its input interface before constructing the ICMP error. This matches similar initialization normally performed by ip_rcv_core() and ip6_rcv_core(). Fixes: 117ca1f8920c ("netfilter: nft_reject_inet: allow to use reject from inet ingress") Cc: stable@vger.kernel.org Assisted-by: Codex:gpt-5.6-sol Codex:gpt-5.5-cyber Signed-off-by: Kyle Zeng Co-developed-by: David Lee Signed-off-by: David Lee Signed-off-by: Pablo Neira Ayuso --- net/ipv4/netfilter/nf_reject_ipv4.c | 5 +++++ net/ipv6/netfilter/nf_reject_ipv6.c | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/net/ipv4/netfilter/nf_reject_ipv4.c b/net/ipv4/netfilter/nf_reject_ipv4.c index 59ec465a9df9..7cb33af671e2 100644 --- a/net/ipv4/netfilter/nf_reject_ipv4.c +++ b/net/ipv4/netfilter/nf_reject_ipv4.c @@ -357,6 +357,11 @@ void nf_send_unreach(struct sk_buff *skb_in, int code, int hook) if (!skb_valid_dst(skb_in) && nf_reject_fill_skb_dst(skb_in) < 0) return; + if (hook == NF_INET_INGRESS) { + memset(IPCB(skb_in), 0, sizeof(*IPCB(skb_in))); + IPCB(skb_in)->iif = skb_in->skb_iif; + } + if (skb_csum_unnecessary(skb_in) || !nf_reject_verify_csum(skb_in, dataoff, proto)) { icmp_send(skb_in, ICMP_DEST_UNREACH, code, 0); diff --git a/net/ipv6/netfilter/nf_reject_ipv6.c b/net/ipv6/netfilter/nf_reject_ipv6.c index 07cdaa10da0d..5e08d8529cc8 100644 --- a/net/ipv6/netfilter/nf_reject_ipv6.c +++ b/net/ipv6/netfilter/nf_reject_ipv6.c @@ -444,6 +444,11 @@ void nf_send_unreach6(struct net *net, struct sk_buff *skb_in, if (hooknum == NF_INET_LOCAL_OUT && skb_in->dev == NULL) skb_in->dev = net->loopback_dev; + if (hooknum == NF_INET_INGRESS) { + memset(IP6CB(skb_in), 0, sizeof(*IP6CB(skb_in))); + IP6CB(skb_in)->iif = skb_in->skb_iif; + } + if (!skb_valid_dst(skb_in) && nf_reject6_fill_skb_dst(skb_in) < 0) return; From 86d47d4c84a1b8470ba3fc16df13ea2a220b74ff Mon Sep 17 00:00:00 2001 From: Pablo Neira Ayuso Date: Wed, 7 Oct 2026 23:51:44 +0200 Subject: [PATCH 07/10] netfilter: nft_set_rbtree: narrow down range completion to anonymous sets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For anonymous sets, when adjacent ranges are found, the end element is not added to the set to pack the set representation. Use next start element to complete this interval. Jérémy Jean says: Consider two ranges in a netfilter verdict map: * keys 10-19 accept packets, and * keys 30-39 jump to chain 'victim'. Now, delete the end marker at 20. The lookup array now uses element 30 as the first range's end, so an INTERVAL_END query for key 15 returns element 30, including its jump verdict and chain pointer. This can make KASAN reports a UAF after the GC releases reference chain for 30 that has expired: BUG: KASAN: slab-use-after-free in nft_verdict_dump+0xdd/0x110 Read of size 8 at addr ff110000009dfb58 by task repro/50 ... Call Trace: ... nft_verdict_dump+0xdd/0x110 nft_data_dump+0x4d/0x80 nf_tables_fill_setelem+0x30b/0x8a0 ? __asan_memcpy+0x3c/0x60 nf_tables_fill_setelem_info+0x1b0/0x200 nft_get_set_elem+0x3fd/0x4b0 ... Freed by task 24: ... kfree+0x1a5/0x280 nf_tables_trans_destroy_work+0x2cb/0x520 Fixes: 2aa34191f06f ("netfilter: nft_set_rbtree: use binary search array in get command") Reported-by: Jérémy Jean Link: https://patch.msgid.link/netfilter-devel/20261005200404.3544062-2-Jeremy.Jean@oss.cyber.gouv.fr/ Signed-off-by: Pablo Neira Ayuso --- net/netfilter/nft_set_rbtree.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/net/netfilter/nft_set_rbtree.c b/net/netfilter/nft_set_rbtree.c index 12431b55752f..fbe1750f1ca2 100644 --- a/net/netfilter/nft_set_rbtree.c +++ b/net/netfilter/nft_set_rbtree.c @@ -1091,7 +1091,8 @@ static void nft_rbtree_commit(struct nft_set *set) * representation. Use next start element to complete this * interval. */ - if (nft_rbtree_interval_start(rbe) && + if (nft_set_is_anonymous(set) && + nft_rbtree_interval_start(rbe) && nft_rbtree_interval_start(prev_rbe) && priv->array_next->intervals[num_intervals].from) priv->array_next->intervals[num_intervals++].to = &prev_rbe->ext; From 2edda78aa4eb04cd8f112a81897510ee0cdd7099 Mon Sep 17 00:00:00 2001 From: Sung Byeongchan Date: Fri, 9 Oct 2026 03:38:14 +0900 Subject: [PATCH 08/10] netfilter: xt_dccp: handle reserved options as single-byte The DCCP UAPI defines option numbers 3 through 31 as reserved. Like Padding and Mandatory, these options occupy one byte. dccp_find_option() only treats values below 2 as single-byte and interprets the byte after a reserved option as a TLV length. This can skip a later option and invert an XT_DCCP_OPTION rule result. A local production-hook fixture installed the same DROP rule for IPv4 and IPv6. In three fresh boots per kernel, a target-only packet was dropped 20/20 times, while adding reserved option 3 before the same target let 20/20 packets reach the protected receiver. The fixed kernel dropped all trigger packets in three fresh boots and preserved the normal controls. Treat every option up to DCCPO_MAX_RESERVED as single-byte, matching the protocol definition. Triggering requires CONFIG_NETFILTER_XTABLES_LEGACY, CONFIG_NETFILTER_XT_MATCH_DCCP, an option-based rule, and remote protocol-33 reachability. The touched source is unchanged at 0c2669a9f4a1d607e7591ae50ccf3c432a0aff08, where the patch applies and builds cleanly. Fixes: 1d3de414eb20 ("[NETFILTER]: New iptables DCCP protocol header match") Assisted-by: LLM Signed-off-by: Sung Byeongchan Signed-off-by: Pablo Neira Ayuso --- net/netfilter/xt_dccp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/net/netfilter/xt_dccp.c b/net/netfilter/xt_dccp.c index 3db81e041af9..23a4e3e8df7d 100644 --- a/net/netfilter/xt_dccp.c +++ b/net/netfilter/xt_dccp.c @@ -62,7 +62,7 @@ dccp_find_option(u_int8_t option, return true; } - if (op[i] < 2 || i == optlen - 1) + if (op[i] <= DCCPO_MAX_RESERVED || i == optlen - 1) i++; else i += op[i + 1] ? : 1; From 08f7882637f2db5b98f521498eeaf94770383a6e Mon Sep 17 00:00:00 2001 From: Lorenzo Bianconi Date: Mon, 19 Oct 2026 11:54:03 +0200 Subject: [PATCH 09/10] netfilter: flowtable: reset transport header after nf_flow_encap_pop() nf_flow_encap_pop() pulls vlan/pppoe or ip tunnel headers and resets the network header, but leaves the transport header pointing into the removed outer header. Reset the transport header to the inner L4 header, otherwise later consumers (e.g. GSO segmentation) parse a bogus header. Fixes: 4cd91f7c290f ("netfilter: flowtable: add vlan support") Signed-off-by: Lorenzo Bianconi Signed-off-by: Pablo Neira Ayuso --- net/netfilter/nf_flow_table_ip.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/net/netfilter/nf_flow_table_ip.c b/net/netfilter/nf_flow_table_ip.c index 2dec15d402ca..bb894c4f2b4a 100644 --- a/net/netfilter/nf_flow_table_ip.c +++ b/net/netfilter/nf_flow_table_ip.c @@ -502,6 +502,7 @@ static int nf_flow_offload_forward(struct nf_flowtable_ctx *ctx, nf_flow_encap_pop(ctx, skb, tuplehash); thoff -= ctx->offset; + skb_set_transport_header(skb, thoff); iph = ip_hdr(skb); nf_flow_nat_ip(flow, skb, thoff, dir, iph); @@ -1097,6 +1098,7 @@ static int nf_flow_offload_ipv6_forward(struct nf_flowtable_ctx *ctx, flow_offload_refresh(flow_table, flow, false); nf_flow_encap_pop(ctx, skb, tuplehash); + skb_set_transport_header(skb, sizeof(*ip6h)); ip6h = ipv6_hdr(skb); nf_flow_nat_ipv6(flow, skb, dir, ip6h); From cd37ab22a74e05aae5c16c67295cbf5188b16268 Mon Sep 17 00:00:00 2001 From: Liu Chao Date: Mon, 19 Oct 2026 15:56:26 +0800 Subject: [PATCH 10/10] netfilter: nf_conntrack_netlink: fix inverted IPv6 tuple filter ctnetlink_filter_match_tuple() returns 0 (no match) when the IPv6 source or destination address differs from the filter, because the !ipv6_addr_cmp() test is inverted. ipv6_addr_cmp() returns 0 when the addresses are equal, so the negation makes the branch trigger on a match instead of a mismatch, which is the opposite of the IPv4 path above. Drop the '!' so the IPv6 branch returns 0 only when the addresses differ, matching the IPv4 semantics and the caller's expectation. Fixes: cb8aa9a3affb ("netfilter: ctnetlink: add kernel side filtering for dump") Cc: stable@vger.kernel.org Signed-off-by: Liu Chao Reviewed-by: Weibin Liu Reviewed-by: Florian Westphal Signed-off-by: Pablo Neira Ayuso --- net/netfilter/nf_conntrack_netlink.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/net/netfilter/nf_conntrack_netlink.c b/net/netfilter/nf_conntrack_netlink.c index 4e5d7c701436..cc8695d5356d 100644 --- a/net/netfilter/nf_conntrack_netlink.c +++ b/net/netfilter/nf_conntrack_netlink.c @@ -1098,13 +1098,13 @@ static int ctnetlink_filter_match_tuple(struct nf_conntrack_tuple *filter_tuple, break; case NFPROTO_IPV6: if ((flags & CTA_FILTER_FLAG(CTA_IP_SRC)) && - !ipv6_addr_cmp(&filter_tuple->src.u3.in6, - &ct_tuple->src.u3.in6)) + ipv6_addr_cmp(&filter_tuple->src.u3.in6, + &ct_tuple->src.u3.in6)) return 0; if ((flags & CTA_FILTER_FLAG(CTA_IP_DST)) && - !ipv6_addr_cmp(&filter_tuple->dst.u3.in6, - &ct_tuple->dst.u3.in6)) + ipv6_addr_cmp(&filter_tuple->dst.u3.in6, + &ct_tuple->dst.u3.in6)) return 0; break; }