* [PATCH net 01/10] ipvs: stop estimator after disabled calc phase
2026-07-31 15:17 [PATCH net 00/10] Netfilter/IPVS fixes for net Pablo Neira Ayuso
@ 2026-07-31 15:17 ` Pablo Neira Ayuso
2026-07-31 15:17 ` [PATCH net 02/10] netfilter: ebt_nflog: pin the NFLOG backend Pablo Neira Ayuso
` (8 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Pablo Neira Ayuso @ 2026-07-31 15:17 UTC (permalink / raw)
To: netfilter-devel; +Cc: davem, netdev, kuba, pabeni, edumazet, fw, horms
From: Zhiling Zou <zhilinz@nebusec.ai>
IPVS estimator kthread 0 starts with zeroed chain and tick limits until
its initial calculation phase completes. If network namespace teardown
clears ipvs->enable during that phase, ip_vs_est_calc_phase() can return
without installing positive limits.
The kthread can then continue into its main loop and drain
est_temp_list with zero chain_max, tick_max and est_max_count values.
Each enqueue consumes one available tick row, but est_count never
reaches the zero est_max_count value. After all rows are consumed, the
row lookup returns IPVS_EST_NTICKS and ip_vs_enqueue_estimator() writes
past the ticks and tick_len arrays.
Exit kthread 0 after the calculation phase if the kthread is stopping or
IPVS has been disabled. That keeps temporary estimators from being
drained after the limits failed to initialize.
Estimator kthreads can now self-exit before teardown or reload stops
kd->task. Keep an extra task reference after creation and release it
with kthread_stop_put(), so kd->task remains valid until the stop paths
consume that reference.
Fixes: 705dd3444081 ("ipvs: use kthreads for stats estimation")
Cc: stable@vger.kernel.org
Reported-by: Vega <vega@nebusec.ai>
Signed-off-by: Zhiling Zou <zhilinz@nebusec.ai>
Acked-by: Julian Anastasov <ja@ssi.bg>
Signed-off-by: Pablo Neira Ayuso <pablo@netfilter.org>
---
net/netfilter/ipvs/ip_vs_est.c | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/net/netfilter/ipvs/ip_vs_est.c b/net/netfilter/ipvs/ip_vs_est.c
index ab09f5182951..05a216a47b45 100644
--- a/net/netfilter/ipvs/ip_vs_est.c
+++ b/net/netfilter/ipvs/ip_vs_est.c
@@ -191,8 +191,11 @@ static int ip_vs_estimation_kthread(void *data)
}
/* kthread 0 will handle the calc phase */
- if (ipvs->est_calc_phase)
+ if (ipvs->est_calc_phase) {
ip_vs_est_calc_phase(ipvs);
+ if (kthread_should_stop() || !READ_ONCE(ipvs->enable))
+ return 0;
+ }
}
while (1) {
@@ -270,6 +273,7 @@ int ip_vs_est_kthread_start(struct netns_ipvs *ipvs,
kd->task = NULL;
goto out;
}
+ get_task_struct(kd->task);
set_user_nice(kd->task, sysctl_est_nice(ipvs));
if (sysctl_est_preferred_cpulist(ipvs))
@@ -286,7 +290,7 @@ void ip_vs_est_kthread_stop(struct ip_vs_est_kt_data *kd)
{
if (kd->task) {
pr_info("stopping estimator thread %d...\n", kd->id);
- kthread_stop(kd->task);
+ kthread_stop_put(kd->task);
kd->task = NULL;
}
}
@@ -526,7 +530,7 @@ static void ip_vs_est_kthread_destroy(struct ip_vs_est_kt_data *kd)
if (kd) {
if (kd->task) {
pr_info("stop unused estimator thread %d...\n", kd->id);
- kthread_stop(kd->task);
+ kthread_stop_put(kd->task);
}
ip_vs_stats_free(kd->calc_stats);
kfree(kd);
--
2.47.3
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH net 02/10] netfilter: ebt_nflog: pin the NFLOG backend
2026-07-31 15:17 [PATCH net 00/10] Netfilter/IPVS fixes for net Pablo Neira Ayuso
2026-07-31 15:17 ` [PATCH net 01/10] ipvs: stop estimator after disabled calc phase Pablo Neira Ayuso
@ 2026-07-31 15:17 ` Pablo Neira Ayuso
2026-07-31 15:17 ` [PATCH net 03/10] netfilter: ipset: rework cidr bookkeeping Pablo Neira Ayuso
` (7 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Pablo Neira Ayuso @ 2026-07-31 15:17 UTC (permalink / raw)
To: netfilter-devel; +Cc: davem, netdev, kuba, pabeni, edumazet, fw, horms
From: Chengfeng Ye <nicoyip.dev@gmail.com>
nf_log_unregister() runs after the per-net teardown so its final RCU
grace period also drains readers that obtained the logger from a per-net
binding. However, ebt_nflog passes an explicit ULOG log type to
nf_log_packet() without holding a reference on the selected logger module,
unlike the xt_NFLOG and nft_log frontends.
An ebtables nflog rule can therefore remain callable while nfnetlink_log
is unloaded. The resulting interleaving is:
CPU 0 CPU 1
nfnetlink_log_fini()
unregister_pernet_subsys()
kfree(nfnl_log_pernet(net))
ebt_nflog_tg()
nf_log_packet()
nfulnl_log_packet()
instance_lookup_get_rcu()
The global ULOG logger is still registered at this point, so CPU 1
dereferences the per-net state after CPU 0 has freed it. KASAN reported:
BUG: KASAN: slab-use-after-free in instance_lookup_get_rcu
Read of size 8 at addr ff110001052e6210 by task poc/92
Call Trace:
instance_lookup_get_rcu+0x1ce/0x1f0 [nfnetlink_log]
nfulnl_log_packet+0x248/0x2fb0 [nfnetlink_log]
nf_log_packet+0x204/0x300
ebt_nflog_tg+0x351/0x550
ebt_do_table+0xedf/0x22b0
Allocated by task 90:
__kmalloc_noprof+0x186/0x470
ops_init+0x6d/0x420
register_pernet_operations+0x2f6/0x670
register_pernet_subsys+0x23/0x40
Freed by task 93:
kfree+0x131/0x3c0
ops_undo_list+0x3e3/0x700
unregister_pernet_operations+0x232/0x490
unregister_pernet_subsys+0x1c/0x30
nfnetlink_log_fini+0x34/0x450 [nfnetlink_log]
Acquire the ULOG logger module reference when an ebt_nflog rule is
validated and release it when the rule is destroyed. Request the NFLOG
backend for legacy callers when needed, matching xt_NFLOG. This prevents
module teardown until all ebt_nflog rules have stopped using the logger.
Fixes: c83fa19603bd ("netfilter: nf_log: don't call synchronize_rcu in nf_log_unset")
Cc: stable@vger.kernel.org
Signed-off-by: Chengfeng Ye <nicoyip.dev@gmail.com>
Signed-off-by: Pablo Neira Ayuso <pablo@netfilter.org>
---
net/bridge/netfilter/ebt_nflog.c | 17 ++++++++++++++++-
1 file changed, 16 insertions(+), 1 deletion(-)
diff --git a/net/bridge/netfilter/ebt_nflog.c b/net/bridge/netfilter/ebt_nflog.c
index 61bf8f4465ab..426f8adc912c 100644
--- a/net/bridge/netfilter/ebt_nflog.c
+++ b/net/bridge/netfilter/ebt_nflog.c
@@ -41,11 +41,25 @@ ebt_nflog_tg(struct sk_buff *skb, const struct xt_action_param *par)
static int ebt_nflog_tg_check(const struct xt_tgchk_param *par)
{
struct ebt_nflog_info *info = par->targinfo;
+ int ret;
if (info->flags & ~EBT_NFLOG_MASK)
return -EINVAL;
info->prefix[EBT_NFLOG_PREFIX_SIZE - 1] = '\0';
- return 0;
+
+ ret = nf_logger_find_get(par->family, NF_LOG_TYPE_ULOG);
+ if (ret != 0 && !par->nft_compat) {
+ request_module("%s", "nfnetlink_log");
+
+ ret = nf_logger_find_get(par->family, NF_LOG_TYPE_ULOG);
+ }
+
+ return ret;
+}
+
+static void ebt_nflog_tg_destroy(const struct xt_tgdtor_param *par)
+{
+ nf_logger_put(par->family, NF_LOG_TYPE_ULOG);
}
static struct xt_target ebt_nflog_tg_reg __read_mostly = {
@@ -54,6 +68,7 @@ static struct xt_target ebt_nflog_tg_reg __read_mostly = {
.family = NFPROTO_BRIDGE,
.target = ebt_nflog_tg,
.checkentry = ebt_nflog_tg_check,
+ .destroy = ebt_nflog_tg_destroy,
.targetsize = sizeof(struct ebt_nflog_info),
.me = THIS_MODULE,
};
--
2.47.3
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH net 03/10] netfilter: ipset: rework cidr bookkeeping
2026-07-31 15:17 [PATCH net 00/10] Netfilter/IPVS fixes for net Pablo Neira Ayuso
2026-07-31 15:17 ` [PATCH net 01/10] ipvs: stop estimator after disabled calc phase Pablo Neira Ayuso
2026-07-31 15:17 ` [PATCH net 02/10] netfilter: ebt_nflog: pin the NFLOG backend Pablo Neira Ayuso
@ 2026-07-31 15:17 ` Pablo Neira Ayuso
2026-07-31 15:18 ` [PATCH net 04/10] netfilter: ipset: switch ext_size to atomic64_t Pablo Neira Ayuso
` (6 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Pablo Neira Ayuso @ 2026-07-31 15:17 UTC (permalink / raw)
To: netfilter-devel; +Cc: davem, netdev, kuba, pabeni, edumazet, fw, horms
From: Jozsef Kadlecsik <kadlec@netfilter.org>
According to sashiko, the current bookkeeping of cidr values are unsafe
on weakly-ordered architectures. Replace the in-place updating with an
RCU based method: create the new bookeeping structure, update and replace
the old one with the new. Downside that we need to allocate memory when
deleting a cidr entry - in case of memory pressure fall back to leave holes
which possibility is taken into account at evaluation time.
Thanks to Pablo (Pablo Neira Ayuso <pablo@netfilter.org>) and Cyntia
(Cynthia <cynthia@kosmx.dev>) for helping me in debugging which resulted
the patch "netfilter: ipset: allocate the proper memory for the generic
hash structure" on which this very patch depends.
Signed-off-by: Jozsef Kadlecsik <kadlec@netfilter.org>
Signed-off-by: Florian Westphal <fw@strlen.de>
Signed-off-by: Pablo Neira Ayuso <pablo@netfilter.org>
---
net/netfilter/ipset/ip_set_hash_gen.h | 235 +++++++++++++------
net/netfilter/ipset/ip_set_hash_ipportnet.c | 4 +-
net/netfilter/ipset/ip_set_hash_net.c | 4 +-
net/netfilter/ipset/ip_set_hash_netiface.c | 4 +-
net/netfilter/ipset/ip_set_hash_netnet.c | 12 +-
net/netfilter/ipset/ip_set_hash_netport.c | 4 +-
net/netfilter/ipset/ip_set_hash_netportnet.c | 12 +-
7 files changed, 182 insertions(+), 93 deletions(-)
diff --git a/net/netfilter/ipset/ip_set_hash_gen.h b/net/netfilter/ipset/ip_set_hash_gen.h
index b2d77973272d..dd31992c915c 100644
--- a/net/netfilter/ipset/ip_set_hash_gen.h
+++ b/net/netfilter/ipset/ip_set_hash_gen.h
@@ -99,9 +99,15 @@ struct htable {
#endif
/* Book-keeping of the prefixes added to the set */
+struct net_prefix {
+ u8 cidr; /* the cidr value */
+ u32 count; /* number of elements of this cidr */
+};
+
struct net_prefixes {
- u32 nets[IPSET_NET_COUNT]; /* number of elements for this cidr */
- u8 cidr[IPSET_NET_COUNT]; /* the cidr value */
+ struct rcu_head rcu;
+ u8 len;
+ struct net_prefix nets[] __counted_by(len);
};
/* Compute the hash table size */
@@ -127,11 +133,6 @@ htable_size(u8 hbits)
#else
#define __CIDR(cidr, i) (cidr)
#endif
-
-/* cidr + 1 is stored in net_prefixes to support /0 */
-#define NCIDR_PUT(cidr) ((cidr) + 1)
-#define NCIDR_GET(cidr) ((cidr) - 1)
-
#ifdef IP_SET_HASH_WITH_NETS_PACKED
/* When cidr is packed with nomatch, cidr - 1 is stored in the data entry */
#define DCIDR_PUT(cidr) ((cidr) - 1)
@@ -141,21 +142,11 @@ htable_size(u8 hbits)
#define DCIDR_GET(cidr, i) __CIDR(cidr, i)
#endif
-#define INIT_CIDR(cidr, host_mask) \
- DCIDR_PUT(((cidr) ? NCIDR_GET(cidr) : host_mask))
-
-#ifdef IP_SET_HASH_WITH_NET0
-/* cidr from 0 to HOST_MASK value and c = cidr + 1 */
-#define NLEN (HOST_MASK + 1)
-#define CIDR_POS(c) ((c) - 1)
-#else
-/* cidr from 1 to HOST_MASK value and c = cidr + 1 */
-#define NLEN HOST_MASK
-#define CIDR_POS(c) ((c) - 2)
-#endif
+#define INIT_CIDR(n, host_mask) ({ \
+ const struct net_prefixes *__n = rcu_dereference(n); \
+ DCIDR_PUT((__n)->len ? (__n)->nets[0].cidr : host_mask);\
+})
-#else
-#define NLEN 0
#endif /* IP_SET_HASH_WITH_NETS */
#define SET_ELEM_EXPIRED(set, d) \
@@ -292,6 +283,7 @@ static const union nf_inet_addr zeromask = {};
/* The generic hash structure */
struct htype {
struct htable __rcu *table; /* the hash table */
+ struct net_prefixes __rcu *rnets[IPSET_NET_COUNT]; /* cidr prefixes */
struct htable_gc gc; /* gc workqueue */
u32 maxelem; /* max elements in the hash */
u32 initval; /* random jhash init value */
@@ -302,9 +294,6 @@ struct htype {
#if defined(IP_SET_HASH_WITH_NETMASK) || defined(IP_SET_HASH_WITH_BITMASK)
u8 netmask; /* netmask value for subnets to store */
union nf_inet_addr bitmask; /* stores bitmask */
-#endif
-#ifdef IP_SET_HASH_WITH_NETS
- struct net_prefixes nets[NLEN]; /* book-keeping of prefixes */
#endif
/* Because 'next' is IPv4/IPv6 dependent, no elements of this
* structure and referred in create() may come after 'next'.
@@ -326,50 +315,92 @@ struct mtype_resize_ad {
/* Network cidr size book keeping when the hash stores different
* sized networks. cidr == real cidr + 1 to support /0.
*/
-static void
+static int
mtype_add_cidr(struct ip_set *set, struct htype *h, u8 cidr, u8 n)
{
- int i, j;
+ struct net_prefixes *nets, *tmp;
+ int i, j, found, len = 0, ret = 0;
spin_lock_bh(&set->lock);
+ nets = __ipset_dereference(h->rnets[n]);
/* Add in increasing prefix order, so larger cidr first */
- for (i = 0, j = -1; i < NLEN && h->nets[i].cidr[n]; i++) {
- if (j != -1) {
+ for (i = 0, found = -1; i < nets->len; i++) {
+ if (nets->nets[i].count)
+ len++;
+ if (found != -1) {
continue;
- } else if (h->nets[i].cidr[n] < cidr) {
- j = i;
- } else if (h->nets[i].cidr[n] == cidr) {
- h->nets[CIDR_POS(cidr)].nets[n]++;
+ } else if (nets->nets[i].cidr < cidr) {
+ found = i;
+ } else if (nets->nets[i].cidr == cidr) {
+ nets->nets[i].count++;
goto unlock;
}
}
- if (j != -1) {
- for (; i > j; i--)
- h->nets[i].cidr[n] = h->nets[i - 1].cidr[n];
+ len++;
+ tmp = kzalloc_flex(*tmp, nets, len, GFP_ATOMIC);
+ if (!tmp) {
+ ret = -ENOMEM;
+ goto unlock;
}
- h->nets[i].cidr[n] = cidr;
- h->nets[CIDR_POS(cidr)].nets[n] = 1;
+
+ tmp->len = len;
+ for (i = 0, j = 0; i < nets->len; i++) {
+ if (i == found) {
+ tmp->nets[j].cidr = cidr;
+ tmp->nets[j++].count = 1;
+ }
+ if (!nets->nets[i].count)
+ continue;
+ tmp->nets[j].cidr = nets->nets[i].cidr;
+ tmp->nets[j++].count = nets->nets[i].count;
+ }
+ if (found == -1) {
+ tmp->nets[j].cidr = cidr;
+ tmp->nets[j].count = 1;
+ }
+ rcu_assign_pointer(h->rnets[n], tmp);
+ kfree_rcu(nets, rcu);
unlock:
spin_unlock_bh(&set->lock);
+ return ret;
}
static void
mtype_del_cidr(struct ip_set *set, struct htype *h, u8 cidr, u8 n)
{
- u8 i, j, net_end = NLEN - 1;
+ struct net_prefixes *nets, *tmp;
+ u8 i, j, len = 0;
+ int found;
spin_lock_bh(&set->lock);
- for (i = 0; i < NLEN; i++) {
- if (h->nets[i].cidr[n] != cidr)
- continue;
- h->nets[CIDR_POS(cidr)].nets[n]--;
- if (h->nets[CIDR_POS(cidr)].nets[n] > 0)
- goto unlock;
- for (j = i; j < net_end && h->nets[j].cidr[n]; j++)
- h->nets[j].cidr[n] = h->nets[j + 1].cidr[n];
- h->nets[j].cidr[n] = 0;
+ nets = __ipset_dereference(h->rnets[n]);
+ for (i = 0, found = -1; i < nets->len; i++) {
+ if (nets->nets[i].count)
+ len++;
+ if (nets->nets[i].cidr == cidr)
+ found = i;
+ }
+ if (unlikely(found == -1))
+ goto unlock;
+
+ nets->nets[found].count--;
+ if (nets->nets[found].count)
goto unlock;
+ len--;
+ tmp = kzalloc_flex(*tmp, nets, len, GFP_ATOMIC);
+ if (!tmp)
+ /* Leave a hole */
+ goto unlock;
+
+ tmp->len = len;
+ for (i = 0, j = 0; i < nets->len; i++) {
+ if (!nets->nets[i].count || i == found)
+ continue;
+ tmp->nets[j].cidr = nets->nets[i].cidr;
+ tmp->nets[j++].count = nets->nets[i].count;
}
+ rcu_assign_pointer(h->rnets[n], tmp);
+ kfree_rcu(nets, rcu);
unlock:
spin_unlock_bh(&set->lock);
}
@@ -402,6 +433,9 @@ static void
mtype_flush(struct ip_set *set)
{
struct htype *h = set->data;
+#ifdef IP_SET_HASH_WITH_NETS
+ struct net_prefixes *nets, *tmp;
+#endif
struct htable *t;
struct hbucket *n;
u32 r, i;
@@ -425,7 +459,19 @@ mtype_flush(struct ip_set *set)
spin_unlock_bh(&t->hregion[r].lock);
}
#ifdef IP_SET_HASH_WITH_NETS
- memset(h->nets, 0, sizeof(h->nets));
+ for (i = 0; i < IPSET_NET_COUNT; i++) {
+ nets = ipset_dereference_nfnl(h->rnets[i]);
+ tmp = kzalloc_obj(*tmp, GFP_ATOMIC);
+ if (!tmp) {
+ u8 j;
+
+ for (j = 0; j < nets->len; j++)
+ nets->nets[j].count = 0;
+ } else {
+ rcu_assign_pointer(h->rnets[i], tmp);
+ kfree_rcu(nets, rcu);
+ }
+ }
#endif
}
@@ -433,6 +479,9 @@ mtype_flush(struct ip_set *set)
static void
mtype_ahash_destroy(struct ip_set *set, struct htable *t, bool ext_destroy)
{
+#ifdef IP_SET_HASH_WITH_NETS
+ struct htype *h = set->data;
+#endif
struct hbucket *n;
u32 i;
@@ -446,6 +495,11 @@ mtype_ahash_destroy(struct ip_set *set, struct htable *t, bool ext_destroy)
kfree(n);
}
+#ifdef IP_SET_HASH_WITH_NETS
+ if (ext_destroy)
+ for (i = 0; i < IPSET_NET_COUNT; i++)
+ kfree(rcu_dereference_raw(h->rnets[i]));
+#endif
ip_set_free(t->hregion);
ip_set_free(t);
}
@@ -519,8 +573,7 @@ mtype_gc_do(struct ip_set *set, struct htype *h, struct htable *t, u32 r)
#ifdef IP_SET_HASH_WITH_NETS
for (k = 0; k < IPSET_NET_COUNT; k++)
mtype_del_cidr(set, h,
- NCIDR_PUT(DCIDR_GET(data->cidr, k)),
- k);
+ DCIDR_GET(data->cidr, k), k);
#endif
t->hregion[r].elements--;
ip_set_ext_destroy(set, data);
@@ -950,8 +1003,7 @@ mtype_add(struct ip_set *set, void *value, const struct ip_set_ext *ext,
#ifdef IP_SET_HASH_WITH_NETS
for (i = 0; i < IPSET_NET_COUNT; i++)
mtype_del_cidr(set, h,
- NCIDR_PUT(DCIDR_GET(data->cidr, i)),
- i);
+ DCIDR_GET(data->cidr, i), i);
#endif
ip_set_ext_destroy(set, data);
t->hregion[r].elements--;
@@ -996,7 +1048,7 @@ mtype_add(struct ip_set *set, void *value, const struct ip_set_ext *ext,
t->hregion[r].elements++;
#ifdef IP_SET_HASH_WITH_NETS
for (i = 0; i < IPSET_NET_COUNT; i++)
- mtype_add_cidr(set, h, NCIDR_PUT(DCIDR_GET(d->cidr, i)), i);
+ mtype_add_cidr(set, h, DCIDR_GET(d->cidr, i), i);
#endif
memcpy(data, d, sizeof(struct mtype_elem));
overwrite_extensions:
@@ -1110,7 +1162,7 @@ mtype_del(struct ip_set *set, void *value, const struct ip_set_ext *ext,
#ifdef IP_SET_HASH_WITH_NETS
for (j = 0; j < IPSET_NET_COUNT; j++)
mtype_del_cidr(set, h,
- NCIDR_PUT(DCIDR_GET(d->cidr, j)), j);
+ DCIDR_GET(d->cidr, j), j);
#endif
ip_set_ext_destroy(set, data);
@@ -1193,28 +1245,37 @@ mtype_test_cidrs(struct ip_set *set, struct mtype_elem *d,
{
struct htype *h = set->data;
struct htable *t = rcu_dereference_bh(h->table);
+ struct net_prefixes *nets0;
struct hbucket *n;
struct mtype_elem *data;
#if IPSET_NET_COUNT == 2
+ struct net_prefixes *nets1;
struct mtype_elem orig = *d;
- int ret, i, j = 0, k;
+ int ret, i, j, k;
#else
- int ret, i, j = 0;
+ int ret, i, j;
#endif
u32 key, multi = 0;
u8 pos;
pr_debug("test by nets\n");
- for (; j < NLEN && h->nets[j].cidr[0] && !multi; j++) {
+ rcu_read_lock_bh();
+ nets0 = rcu_dereference_bh(h->rnets[0]);
+#if IPSET_NET_COUNT == 2
+ nets1 = rcu_dereference_bh(h->rnets[1]);
+#endif
+ for (j = 0; j < nets0->len && !multi; j++) {
+ if (!nets0->nets[j].count)
+ continue;
#if IPSET_NET_COUNT == 2
mtype_data_reset_elem(d, &orig);
- mtype_data_netmask(d, NCIDR_GET(h->nets[j].cidr[0]), false);
- for (k = 0; k < NLEN && h->nets[k].cidr[1] && !multi;
- k++) {
- mtype_data_netmask(d, NCIDR_GET(h->nets[k].cidr[1]),
- true);
+ mtype_data_netmask(d, nets0->nets[j].cidr, false);
+ for (k = 0; k < nets1->len && !multi; k++) {
+ if (!nets1->nets[k].count)
+ continue;
+ mtype_data_netmask(d, nets1->nets[k].cidr, true);
#else
- mtype_data_netmask(d, NCIDR_GET(h->nets[j].cidr[0]));
+ mtype_data_netmask(d, nets0->nets[j].cidr);
#endif
key = HKEY(d, h->initval, t->htable_bits);
n = rcu_dereference_bh(hbucket(t, key));
@@ -1229,7 +1290,7 @@ mtype_test_cidrs(struct ip_set *set, struct mtype_elem *d,
continue;
ret = mtype_data_match(data, ext, mext, set, flags);
if (ret != 0)
- return ret;
+ goto unlock;
#ifdef IP_SET_HASH_WITH_MULTI
/* No match, reset multiple match flag */
multi = 0;
@@ -1239,7 +1300,10 @@ mtype_test_cidrs(struct ip_set *set, struct mtype_elem *d,
}
#endif
}
- return 0;
+ ret = 0;
+unlock:
+ rcu_read_unlock_bh();
+ return ret;
}
#endif
@@ -1504,6 +1568,9 @@ IPSET_TOKEN(HTYPE, _create)(struct net *net, struct ip_set *set,
int ret __attribute__((unused)) = 0;
u8 netmask = set->family == NFPROTO_IPV4 ? 32 : 128;
union nf_inet_addr bitmask = onesmask;
+#endif
+#ifdef IP_SET_HASH_WITH_NETS
+ struct net_prefixes *nets;
#endif
size_t hsize;
struct htype *h;
@@ -1604,21 +1671,25 @@ IPSET_TOKEN(HTYPE, _create)(struct net *net, struct ip_set *set,
*/
hbits = fls(hashsize - 1);
hsize = htable_size(hbits);
- if (hsize == 0) {
- kfree(h);
- return -ENOMEM;
- }
+ if (hsize == 0)
+ goto free_h;
t = ip_set_alloc(hsize);
- if (!t) {
- kfree(h);
- return -ENOMEM;
- }
+ if (!t)
+ goto free_h;
t->hregion = ip_set_alloc(ahash_sizeof_regions(hbits));
- if (!t->hregion) {
- ip_set_free(t);
- kfree(h);
- return -ENOMEM;
+ if (!t->hregion)
+ goto free_t;
+#ifdef IP_SET_HASH_WITH_NETS
+ for (i = 0; i < IPSET_NET_COUNT; i++) {
+ nets = kzalloc_obj(*nets);
+ if (!nets) {
+ while (i > 0)
+ kfree(rcu_dereference_raw(h->rnets[--i]));
+ goto free_hregion;
+ }
+ RCU_INIT_POINTER(h->rnets[i], nets);
}
+#endif
h->gc.set = set;
spin_lock_init(&h->gc.lock);
for (i = 0; i < ahash_numof_locks(hbits); i++)
@@ -1682,6 +1753,16 @@ IPSET_TOKEN(HTYPE, _create)(struct net *net, struct ip_set *set,
t->htable_bits, h->maxelem, set->data, t);
return 0;
+
+#ifdef IP_SET_HASH_WITH_NETS
+free_hregion:
+ ip_set_free(t->hregion);
+#endif
+free_t:
+ ip_set_free(t);
+free_h:
+ kfree(h);
+ return -ENOMEM;
}
#endif /* IP_SET_EMIT_CREATE */
diff --git a/net/netfilter/ipset/ip_set_hash_ipportnet.c b/net/netfilter/ipset/ip_set_hash_ipportnet.c
index 2d6652d43199..195853a25b06 100644
--- a/net/netfilter/ipset/ip_set_hash_ipportnet.c
+++ b/net/netfilter/ipset/ip_set_hash_ipportnet.c
@@ -138,7 +138,7 @@ hash_ipportnet4_kadt(struct ip_set *set, const struct sk_buff *skb,
const struct hash_ipportnet4 *h = set->data;
ipset_adtfn adtfn = set->variant->adt[adt];
struct hash_ipportnet4_elem e = {
- .cidr = INIT_CIDR(h->nets[0].cidr[0], HOST_MASK),
+ .cidr = INIT_CIDR(h->rnets[0], HOST_MASK),
};
struct ip_set_ext ext = IP_SET_INIT_KEXT(skb, opt, set);
@@ -398,7 +398,7 @@ hash_ipportnet6_kadt(struct ip_set *set, const struct sk_buff *skb,
const struct hash_ipportnet6 *h = set->data;
ipset_adtfn adtfn = set->variant->adt[adt];
struct hash_ipportnet6_elem e = {
- .cidr = INIT_CIDR(h->nets[0].cidr[0], HOST_MASK),
+ .cidr = INIT_CIDR(h->rnets[0], HOST_MASK),
};
struct ip_set_ext ext = IP_SET_INIT_KEXT(skb, opt, set);
diff --git a/net/netfilter/ipset/ip_set_hash_net.c b/net/netfilter/ipset/ip_set_hash_net.c
index ce0a9ce5a91f..092f3c9281b8 100644
--- a/net/netfilter/ipset/ip_set_hash_net.c
+++ b/net/netfilter/ipset/ip_set_hash_net.c
@@ -117,7 +117,7 @@ hash_net4_kadt(struct ip_set *set, const struct sk_buff *skb,
const struct hash_net4 *h = set->data;
ipset_adtfn adtfn = set->variant->adt[adt];
struct hash_net4_elem e = {
- .cidr = INIT_CIDR(h->nets[0].cidr[0], HOST_MASK),
+ .cidr = INIT_CIDR(h->rnets[0], HOST_MASK),
};
struct ip_set_ext ext = IP_SET_INIT_KEXT(skb, opt, set);
@@ -291,7 +291,7 @@ hash_net6_kadt(struct ip_set *set, const struct sk_buff *skb,
const struct hash_net6 *h = set->data;
ipset_adtfn adtfn = set->variant->adt[adt];
struct hash_net6_elem e = {
- .cidr = INIT_CIDR(h->nets[0].cidr[0], HOST_MASK),
+ .cidr = INIT_CIDR(h->rnets[0], HOST_MASK),
};
struct ip_set_ext ext = IP_SET_INIT_KEXT(skb, opt, set);
diff --git a/net/netfilter/ipset/ip_set_hash_netiface.c b/net/netfilter/ipset/ip_set_hash_netiface.c
index 30a655e5c4fd..b44b95f766b7 100644
--- a/net/netfilter/ipset/ip_set_hash_netiface.c
+++ b/net/netfilter/ipset/ip_set_hash_netiface.c
@@ -161,7 +161,7 @@ hash_netiface4_kadt(struct ip_set *set, const struct sk_buff *skb,
struct hash_netiface4 *h = set->data;
ipset_adtfn adtfn = set->variant->adt[adt];
struct hash_netiface4_elem e = {
- .cidr = INIT_CIDR(h->nets[0].cidr[0], HOST_MASK),
+ .cidr = INIT_CIDR(h->rnets[0], HOST_MASK),
.elem = 1,
};
struct ip_set_ext ext = IP_SET_INIT_KEXT(skb, opt, set);
@@ -382,7 +382,7 @@ hash_netiface6_kadt(struct ip_set *set, const struct sk_buff *skb,
struct hash_netiface6 *h = set->data;
ipset_adtfn adtfn = set->variant->adt[adt];
struct hash_netiface6_elem e = {
- .cidr = INIT_CIDR(h->nets[0].cidr[0], HOST_MASK),
+ .cidr = INIT_CIDR(h->rnets[0], HOST_MASK),
.elem = 1,
};
struct ip_set_ext ext = IP_SET_INIT_KEXT(skb, opt, set);
diff --git a/net/netfilter/ipset/ip_set_hash_netnet.c b/net/netfilter/ipset/ip_set_hash_netnet.c
index 8fbe649c9dd3..f7c8a1cc30fc 100644
--- a/net/netfilter/ipset/ip_set_hash_netnet.c
+++ b/net/netfilter/ipset/ip_set_hash_netnet.c
@@ -149,8 +149,10 @@ hash_netnet4_kadt(struct ip_set *set, const struct sk_buff *skb,
struct hash_netnet4_elem e = { };
struct ip_set_ext ext = IP_SET_INIT_KEXT(skb, opt, set);
- e.cidr[0] = INIT_CIDR(h->nets[0].cidr[0], HOST_MASK);
- e.cidr[1] = INIT_CIDR(h->nets[0].cidr[1], HOST_MASK);
+ rcu_read_lock_bh();
+ e.cidr[0] = INIT_CIDR(h->rnets[0], HOST_MASK);
+ e.cidr[1] = INIT_CIDR(h->rnets[1], HOST_MASK);
+ rcu_read_unlock_bh();
if (adt == IPSET_TEST)
e.ccmp = (HOST_MASK << (sizeof(e.cidr[0]) * 8)) | HOST_MASK;
@@ -388,8 +390,10 @@ hash_netnet6_kadt(struct ip_set *set, const struct sk_buff *skb,
struct hash_netnet6_elem e = { };
struct ip_set_ext ext = IP_SET_INIT_KEXT(skb, opt, set);
- e.cidr[0] = INIT_CIDR(h->nets[0].cidr[0], HOST_MASK);
- e.cidr[1] = INIT_CIDR(h->nets[0].cidr[1], HOST_MASK);
+ rcu_read_lock_bh();
+ e.cidr[0] = INIT_CIDR(h->rnets[0], HOST_MASK);
+ e.cidr[1] = INIT_CIDR(h->rnets[1], HOST_MASK);
+ rcu_read_unlock_bh();
if (adt == IPSET_TEST)
e.ccmp = (HOST_MASK << (sizeof(u8) * 8)) | HOST_MASK;
diff --git a/net/netfilter/ipset/ip_set_hash_netport.c b/net/netfilter/ipset/ip_set_hash_netport.c
index d1a0628df4ef..5de4b511de76 100644
--- a/net/netfilter/ipset/ip_set_hash_netport.c
+++ b/net/netfilter/ipset/ip_set_hash_netport.c
@@ -133,7 +133,7 @@ hash_netport4_kadt(struct ip_set *set, const struct sk_buff *skb,
const struct hash_netport4 *h = set->data;
ipset_adtfn adtfn = set->variant->adt[adt];
struct hash_netport4_elem e = {
- .cidr = INIT_CIDR(h->nets[0].cidr[0], HOST_MASK),
+ .cidr = INIT_CIDR(h->rnets[0], HOST_MASK),
};
struct ip_set_ext ext = IP_SET_INIT_KEXT(skb, opt, set);
@@ -353,7 +353,7 @@ hash_netport6_kadt(struct ip_set *set, const struct sk_buff *skb,
const struct hash_netport6 *h = set->data;
ipset_adtfn adtfn = set->variant->adt[adt];
struct hash_netport6_elem e = {
- .cidr = INIT_CIDR(h->nets[0].cidr[0], HOST_MASK),
+ .cidr = INIT_CIDR(h->rnets[0], HOST_MASK),
};
struct ip_set_ext ext = IP_SET_INIT_KEXT(skb, opt, set);
diff --git a/net/netfilter/ipset/ip_set_hash_netportnet.c b/net/netfilter/ipset/ip_set_hash_netportnet.c
index bf4f91b78e1d..6291532be7a5 100644
--- a/net/netfilter/ipset/ip_set_hash_netportnet.c
+++ b/net/netfilter/ipset/ip_set_hash_netportnet.c
@@ -157,8 +157,10 @@ hash_netportnet4_kadt(struct ip_set *set, const struct sk_buff *skb,
struct hash_netportnet4_elem e = { };
struct ip_set_ext ext = IP_SET_INIT_KEXT(skb, opt, set);
- e.cidr[0] = INIT_CIDR(h->nets[0].cidr[0], HOST_MASK);
- e.cidr[1] = INIT_CIDR(h->nets[0].cidr[1], HOST_MASK);
+ rcu_read_lock_bh();
+ e.cidr[0] = INIT_CIDR(h->rnets[0], HOST_MASK);
+ e.cidr[1] = INIT_CIDR(h->rnets[1], HOST_MASK);
+ rcu_read_unlock_bh();
if (adt == IPSET_TEST)
e.ccmp = (HOST_MASK << (sizeof(e.cidr[0]) * 8)) | HOST_MASK;
@@ -452,8 +454,10 @@ hash_netportnet6_kadt(struct ip_set *set, const struct sk_buff *skb,
struct hash_netportnet6_elem e = { };
struct ip_set_ext ext = IP_SET_INIT_KEXT(skb, opt, set);
- e.cidr[0] = INIT_CIDR(h->nets[0].cidr[0], HOST_MASK);
- e.cidr[1] = INIT_CIDR(h->nets[0].cidr[1], HOST_MASK);
+ rcu_read_lock_bh();
+ e.cidr[0] = INIT_CIDR(h->rnets[0], HOST_MASK);
+ e.cidr[1] = INIT_CIDR(h->rnets[1], HOST_MASK);
+ rcu_read_unlock_bh();
if (adt == IPSET_TEST)
e.ccmp = (HOST_MASK << (sizeof(u8) * 8)) | HOST_MASK;
--
2.47.3
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH net 04/10] netfilter: ipset: switch ext_size to atomic64_t
2026-07-31 15:17 [PATCH net 00/10] Netfilter/IPVS fixes for net Pablo Neira Ayuso
` (2 preceding siblings ...)
2026-07-31 15:17 ` [PATCH net 03/10] netfilter: ipset: rework cidr bookkeeping Pablo Neira Ayuso
@ 2026-07-31 15:18 ` Pablo Neira Ayuso
2026-07-31 15:18 ` [PATCH net 05/10] netfilter: ipset: add small wrappers for hash and bucket sizes Pablo Neira Ayuso
` (5 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Pablo Neira Ayuso @ 2026-07-31 15:18 UTC (permalink / raw)
To: netfilter-devel; +Cc: davem, netdev, kuba, pabeni, edumazet, fw, horms
From: Jozsef Kadlecsik <kadlec@netfilter.org>
The hash types do not acquire set->lock, they use 'region locking' where
only part of the hash table is locked. Parallel inserts and deletes are
possible and CPUs can race on ->ext_size update. Switch to atomic64_t.
This leaves another bug unresolved: there still can be a race on
comment extension re-init. This will be handled in a later commit
when converting to rhashtable backend.
Fixes: f66ee0410b1c ("netfilter: ipset: Fix "INFO: rcu detected stall in hash_xxx" reports")
Signed-off-by: Jozsef Kadlecsik <kadlec@netfilter.org>
Signed-off-by: Florian Westphal <fw@strlen.de>
Signed-off-by: Pablo Neira Ayuso <pablo@netfilter.org>
---
include/linux/netfilter/ipset/ip_set.h | 2 +-
net/netfilter/ipset/ip_set_bitmap_gen.h | 4 ++--
net/netfilter/ipset/ip_set_core.c | 6 +++---
net/netfilter/ipset/ip_set_hash_gen.h | 2 +-
net/netfilter/ipset/ip_set_list_set.c | 4 ++--
5 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/include/linux/netfilter/ipset/ip_set.h b/include/linux/netfilter/ipset/ip_set.h
index b98331572ad2..cadae9b2578f 100644
--- a/include/linux/netfilter/ipset/ip_set.h
+++ b/include/linux/netfilter/ipset/ip_set.h
@@ -273,7 +273,7 @@ struct ip_set {
/* Number of elements (vs timeout) */
u32 elements;
/* Size of the dynamic extensions (vs timeout) */
- size_t ext_size;
+ atomic64_t ext_size;
/* Element data size */
size_t dsize;
/* Offsets to extensions in elements */
diff --git a/net/netfilter/ipset/ip_set_bitmap_gen.h b/net/netfilter/ipset/ip_set_bitmap_gen.h
index bb9b5bed10e1..226fdf17b683 100644
--- a/net/netfilter/ipset/ip_set_bitmap_gen.h
+++ b/net/netfilter/ipset/ip_set_bitmap_gen.h
@@ -77,7 +77,7 @@ mtype_flush(struct ip_set *set)
mtype_ext_cleanup(set);
bitmap_zero(map->members, map->elements);
set->elements = 0;
- set->ext_size = 0;
+ atomic64_set(&set->ext_size, 0);
}
/* Calculate the actual memory size of the set data */
@@ -93,7 +93,7 @@ mtype_head(struct ip_set *set, struct sk_buff *skb)
{
const struct mtype *map = set->data;
struct nlattr *nested;
- size_t memsize = mtype_memsize(map, set->dsize) + set->ext_size;
+ size_t memsize = mtype_memsize(map, set->dsize) + atomic64_read(&set->ext_size);
nested = nla_nest_start(skb, IPSET_ATTR_DATA);
if (!nested)
diff --git a/net/netfilter/ipset/ip_set_core.c b/net/netfilter/ipset/ip_set_core.c
index 6cfad152d7d1..822a53a7f502 100644
--- a/net/netfilter/ipset/ip_set_core.c
+++ b/net/netfilter/ipset/ip_set_core.c
@@ -350,7 +350,7 @@ ip_set_init_comment(struct ip_set *set, struct ip_set_comment *comment,
size_t len = ext->comment ? strlen(ext->comment) : 0;
if (unlikely(c)) {
- set->ext_size -= sizeof(*c) + strlen(c->str) + 1;
+ atomic64_sub(sizeof(*c) + strlen(c->str) + 1, &set->ext_size);
rcu_assign_pointer(comment->c, NULL);
kfree_rcu(c, rcu);
}
@@ -362,7 +362,7 @@ ip_set_init_comment(struct ip_set *set, struct ip_set_comment *comment,
if (unlikely(!c))
return;
strscpy(c->str, ext->comment, len + 1);
- set->ext_size += sizeof(*c) + strlen(c->str) + 1;
+ atomic64_add(sizeof(*c) + strlen(c->str) + 1, &set->ext_size);
rcu_assign_pointer(comment->c, c);
}
EXPORT_SYMBOL_GPL(ip_set_init_comment);
@@ -392,7 +392,7 @@ ip_set_comment_free(struct ip_set *set, void *ptr)
c = rcu_dereference_protected(comment->c, 1);
if (unlikely(!c))
return;
- set->ext_size -= sizeof(*c) + strlen(c->str) + 1;
+ atomic64_sub(sizeof(*c) + strlen(c->str) + 1, &set->ext_size);
rcu_assign_pointer(comment->c, NULL);
kfree_rcu(c, rcu);
}
diff --git a/net/netfilter/ipset/ip_set_hash_gen.h b/net/netfilter/ipset/ip_set_hash_gen.h
index dd31992c915c..8841daf28f01 100644
--- a/net/netfilter/ipset/ip_set_hash_gen.h
+++ b/net/netfilter/ipset/ip_set_hash_gen.h
@@ -1373,7 +1373,7 @@ mtype_head(struct ip_set *set, struct sk_buff *skb)
rcu_read_lock_bh();
t = rcu_dereference_bh(h->table);
mtype_ext_size(set, &elements, &ext_size);
- memsize = mtype_ahash_memsize(h, t) + ext_size + set->ext_size;
+ memsize = mtype_ahash_memsize(h, t) + ext_size + atomic64_read(&set->ext_size);
htable_bits = t->htable_bits;
rcu_read_unlock_bh();
diff --git a/net/netfilter/ipset/ip_set_list_set.c b/net/netfilter/ipset/ip_set_list_set.c
index 1cef84f15e8c..ca3ef9479e83 100644
--- a/net/netfilter/ipset/ip_set_list_set.c
+++ b/net/netfilter/ipset/ip_set_list_set.c
@@ -421,7 +421,7 @@ list_set_flush(struct ip_set *set)
list_for_each_entry_safe(e, n, &map->members, list)
list_set_del(set, e);
set->elements = 0;
- set->ext_size = 0;
+ atomic64_set(&set->ext_size, 0);
}
static void
@@ -455,7 +455,7 @@ list_set_head(struct ip_set *set, struct sk_buff *skb)
{
const struct list_set *map = set->data;
struct nlattr *nested;
- size_t memsize = list_set_memsize(map, set->dsize) + set->ext_size;
+ size_t memsize = list_set_memsize(map, set->dsize) + atomic64_read(&set->ext_size);
nested = nla_nest_start(skb, IPSET_ATTR_DATA);
if (!nested)
--
2.47.3
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH net 05/10] netfilter: ipset: add small wrappers for hash and bucket sizes
2026-07-31 15:17 [PATCH net 00/10] Netfilter/IPVS fixes for net Pablo Neira Ayuso
` (3 preceding siblings ...)
2026-07-31 15:18 ` [PATCH net 04/10] netfilter: ipset: switch ext_size to atomic64_t Pablo Neira Ayuso
@ 2026-07-31 15:18 ` Pablo Neira Ayuso
2026-07-31 15:18 ` [PATCH net 06/10] netfilter: ipset: add and use mtype_del_cidr_all helper Pablo Neira Ayuso
` (4 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Pablo Neira Ayuso @ 2026-07-31 15:18 UTC (permalink / raw)
To: netfilter-devel; +Cc: davem, netdev, kuba, pabeni, edumazet, fw, horms
From: Florian Westphal <fw@strlen.de>
Preparation patch. Once the ipset hash table is replaced with rhashtable
these functions are needed. Add them in extra commit to have reviewable
chunks.
Assisted-by: Claude:claude-sonnet-4-6
Signed-off-by: Florian Westphal <fw@strlen.de>
Signed-off-by: Pablo Neira Ayuso <pablo@netfilter.org>
---
net/netfilter/ipset/ip_set_hash_gen.h | 39 +++++++++++++++++++++------
1 file changed, 31 insertions(+), 8 deletions(-)
diff --git a/net/netfilter/ipset/ip_set_hash_gen.h b/net/netfilter/ipset/ip_set_hash_gen.h
index 8841daf28f01..ef586b486f51 100644
--- a/net/netfilter/ipset/ip_set_hash_gen.h
+++ b/net/netfilter/ipset/ip_set_hash_gen.h
@@ -201,6 +201,8 @@ static const union nf_inet_addr zeromask = {};
#undef mtype_same_set
#undef mtype_kadt
#undef mtype_uadt
+#undef mtype_bucket_size
+#undef mtype_hash_size
#undef mtype_add
#undef mtype_del
@@ -246,6 +248,8 @@ static const union nf_inet_addr zeromask = {};
#define mtype_same_set IPSET_TOKEN(MTYPE, _same_set)
#define mtype_kadt IPSET_TOKEN(MTYPE, _kadt)
#define mtype_uadt IPSET_TOKEN(MTYPE, _uadt)
+#define mtype_bucket_size IPSET_TOKEN(MTYPE, _bucket_size)
+#define mtype_hash_size IPSET_TOKEN(MTYPE, _hash_size)
#define mtype_add IPSET_TOKEN(MTYPE, _add)
#define mtype_del IPSET_TOKEN(MTYPE, _del)
@@ -1358,6 +1362,24 @@ mtype_test(struct ip_set *set, void *value, const struct ip_set_ext *ext,
return ret;
}
+static u32 mtype_hash_size(const struct htype *h)
+{
+ const struct htable *t;
+ u8 htable_bits;
+
+ rcu_read_lock();
+ t = rcu_dereference(h->table);
+ htable_bits = t->htable_bits;
+ rcu_read_unlock();
+
+ return jhash_size(htable_bits);
+}
+
+static u32 mtype_bucket_size(const struct htype *h)
+{
+ return h->bucketsize;
+}
+
/* Reply a HEADER request: fill out the header part of the set */
static int
mtype_head(struct ip_set *set, struct sk_buff *skb)
@@ -1368,21 +1390,20 @@ mtype_head(struct ip_set *set, struct sk_buff *skb)
size_t memsize;
u32 elements = 0;
size_t ext_size = 0;
- u8 htable_bits;
rcu_read_lock_bh();
t = rcu_dereference_bh(h->table);
mtype_ext_size(set, &elements, &ext_size);
memsize = mtype_ahash_memsize(h, t) + ext_size + atomic64_read(&set->ext_size);
- htable_bits = t->htable_bits;
rcu_read_unlock_bh();
nested = nla_nest_start(skb, IPSET_ATTR_DATA);
if (!nested)
goto nla_put_failure;
- if (nla_put_net32(skb, IPSET_ATTR_HASHSIZE,
- htonl(jhash_size(htable_bits))) ||
- nla_put_net32(skb, IPSET_ATTR_MAXELEM, htonl(h->maxelem)))
+
+ if (nla_put_net32(skb, IPSET_ATTR_HASHSIZE, htonl(mtype_hash_size(h))))
+ goto nla_put_failure;
+ if (nla_put_net32(skb, IPSET_ATTR_MAXELEM, htonl(h->maxelem)))
goto nla_put_failure;
#ifdef IP_SET_HASH_WITH_BITMASK
/* if netmask is set to anything other than HOST_MASK we know that the user supplied netmask
@@ -1406,8 +1427,9 @@ mtype_head(struct ip_set *set, struct sk_buff *skb)
goto nla_put_failure;
#endif
if (set->flags & IPSET_CREATE_FLAG_BUCKETSIZE) {
- if (nla_put_u8(skb, IPSET_ATTR_BUCKETSIZE, h->bucketsize) ||
- nla_put_net32(skb, IPSET_ATTR_INITVAL, htonl(h->initval)))
+ if (nla_put_u8(skb, IPSET_ATTR_BUCKETSIZE, mtype_bucket_size(h)))
+ goto nla_put_failure;
+ if (nla_put_net32(skb, IPSET_ATTR_INITVAL, htonl(h->initval)))
goto nla_put_failure;
}
if (nla_put_net32(skb, IPSET_ATTR_REFERENCES, htonl(set->ref)) ||
@@ -1721,6 +1743,7 @@ IPSET_TOKEN(HTYPE, _create)(struct net *net, struct ip_set *set,
INIT_LIST_HEAD(&t->ad);
RCU_INIT_POINTER(h->table, t);
set->data = h;
+
#ifndef IP_SET_PROTO_UNDEF
if (set->family == NFPROTO_IPV4) {
#endif
@@ -1749,7 +1772,7 @@ IPSET_TOKEN(HTYPE, _create)(struct net *net, struct ip_set *set,
#endif
}
pr_debug("create %s hashsize %u (%u) maxelem %u: %p(%p)\n",
- set->name, jhash_size(t->htable_bits),
+ set->name, mtype_hash_size(h),
t->htable_bits, h->maxelem, set->data, t);
return 0;
--
2.47.3
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH net 06/10] netfilter: ipset: add and use mtype_del_cidr_all helper
2026-07-31 15:17 [PATCH net 00/10] Netfilter/IPVS fixes for net Pablo Neira Ayuso
` (4 preceding siblings ...)
2026-07-31 15:18 ` [PATCH net 05/10] netfilter: ipset: add small wrappers for hash and bucket sizes Pablo Neira Ayuso
@ 2026-07-31 15:18 ` Pablo Neira Ayuso
2026-07-31 15:18 ` [PATCH net 07/10] netfilter: ipset: switch to rcu work Pablo Neira Ayuso
` (3 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Pablo Neira Ayuso @ 2026-07-31 15:18 UTC (permalink / raw)
To: netfilter-devel; +Cc: davem, netdev, kuba, pabeni, edumazet, fw, horms
From: Florian Westphal <fw@strlen.de>
Reduces size of upcoming rhashtable conversion.
Assisted-by: Claude:claude-sonnet-4-6
Signed-off-by: Florian Westphal <fw@strlen.de>
Signed-off-by: Pablo Neira Ayuso <pablo@netfilter.org>
---
net/netfilter/ipset/ip_set_hash_gen.h | 34 +++++++++++++--------------
1 file changed, 16 insertions(+), 18 deletions(-)
diff --git a/net/netfilter/ipset/ip_set_hash_gen.h b/net/netfilter/ipset/ip_set_hash_gen.h
index ef586b486f51..f00c82acd7f0 100644
--- a/net/netfilter/ipset/ip_set_hash_gen.h
+++ b/net/netfilter/ipset/ip_set_hash_gen.h
@@ -195,6 +195,7 @@ static const union nf_inet_addr zeromask = {};
#undef mtype_ext_cleanup
#undef mtype_add_cidr
#undef mtype_del_cidr
+#undef mtype_del_cidr_all
#undef mtype_ahash_memsize
#undef mtype_flush
#undef mtype_destroy
@@ -242,6 +243,7 @@ static const union nf_inet_addr zeromask = {};
#define mtype_ext_cleanup IPSET_TOKEN(MTYPE, _ext_cleanup)
#define mtype_add_cidr IPSET_TOKEN(MTYPE, _add_cidr)
#define mtype_del_cidr IPSET_TOKEN(MTYPE, _del_cidr)
+#define mtype_del_cidr_all IPSET_TOKEN(MTYPE, _del_cidr_all)
#define mtype_ahash_memsize IPSET_TOKEN(MTYPE, _ahash_memsize)
#define mtype_flush IPSET_TOKEN(MTYPE, _flush)
#define mtype_destroy IPSET_TOKEN(MTYPE, _destroy)
@@ -410,6 +412,17 @@ mtype_del_cidr(struct ip_set *set, struct htype *h, u8 cidr, u8 n)
}
#endif
+static void
+mtype_del_cidr_all(struct ip_set *set, struct htype *h, const struct mtype_elem *data)
+{
+#ifdef IP_SET_HASH_WITH_NETS
+ int k;
+
+ for (k = 0; k < IPSET_NET_COUNT; k++)
+ mtype_del_cidr(set, h, DCIDR_GET(data->cidr, k), k);
+#endif
+}
+
/* Calculate the actual memory size of the set data */
static size_t
mtype_ahash_memsize(const struct htype *h, const struct htable *t)
@@ -551,9 +564,6 @@ mtype_gc_do(struct ip_set *set, struct htype *h, struct htable *t, u32 r)
struct mtype_elem *data;
u32 i, j, d;
size_t dsize = set->dsize;
-#ifdef IP_SET_HASH_WITH_NETS
- u8 k;
-#endif
u8 pos, htable_bits = t->htable_bits;
spin_lock_bh(&t->hregion[r].lock);
@@ -574,11 +584,7 @@ mtype_gc_do(struct ip_set *set, struct htype *h, struct htable *t, u32 r)
pr_debug("expired %u/%u\n", i, j);
clear_bit(j, n->used);
smp_mb__after_atomic();
-#ifdef IP_SET_HASH_WITH_NETS
- for (k = 0; k < IPSET_NET_COUNT; k++)
- mtype_del_cidr(set, h,
- DCIDR_GET(data->cidr, k), k);
-#endif
+ mtype_del_cidr_all(set, h, data);
t->hregion[r].elements--;
ip_set_ext_destroy(set, data);
d++;
@@ -1004,11 +1010,7 @@ mtype_add(struct ip_set *set, void *value, const struct ip_set_ext *ext,
j = 0;
data = ahash_data(n, j, set->dsize);
if (!deleted) {
-#ifdef IP_SET_HASH_WITH_NETS
- for (i = 0; i < IPSET_NET_COUNT; i++)
- mtype_del_cidr(set, h,
- DCIDR_GET(data->cidr, i), i);
-#endif
+ mtype_del_cidr_all(set, h, data);
ip_set_ext_destroy(set, data);
t->hregion[r].elements--;
}
@@ -1163,11 +1165,7 @@ mtype_del(struct ip_set *set, void *value, const struct ip_set_ext *ext,
if (i + 1 == pos)
smp_store_release(&n->pos, --pos);
t->hregion[r].elements--;
-#ifdef IP_SET_HASH_WITH_NETS
- for (j = 0; j < IPSET_NET_COUNT; j++)
- mtype_del_cidr(set, h,
- DCIDR_GET(d->cidr, j), j);
-#endif
+ mtype_del_cidr_all(set, h, d);
ip_set_ext_destroy(set, data);
if (t->resizing && ext && ext->target) {
--
2.47.3
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH net 07/10] netfilter: ipset: switch to rcu work
2026-07-31 15:17 [PATCH net 00/10] Netfilter/IPVS fixes for net Pablo Neira Ayuso
` (5 preceding siblings ...)
2026-07-31 15:18 ` [PATCH net 06/10] netfilter: ipset: add and use mtype_del_cidr_all helper Pablo Neira Ayuso
@ 2026-07-31 15:18 ` Pablo Neira Ayuso
2026-07-31 15:18 ` [PATCH net 08/10] ipvs: avoid out-of-bounds write in ip_vs_nat_icmp Pablo Neira Ayuso
` (2 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Pablo Neira Ayuso @ 2026-07-31 15:18 UTC (permalink / raw)
To: netfilter-devel; +Cc: davem, netdev, kuba, pabeni, edumazet, fw, horms
From: Florian Westphal <fw@strlen.de>
In the initial ipset rhashtable conversion RFC series syzbot reported
following splat:
BUG: sleeping function [..] at kernel/irq_work.c:289
in_atomic(): 1, [..]
irq_work_sync.. kernel/irq_work.c:289
rhashtable_free_and_destroy.. lib/rhashtable.c:1295
hash_netport4_destroy.. net/netfilter/ipset/ip_set_hash_gen.h:420
ip_set_destroy_set_rcu.. net/netfilter/ipset/ip_set_core.c:1169
rcu_core.. kernel/rcu/tree.c:2897
This is because post-rhashtable-conversion hash implementation needs
to schedule in the destroy callback. At this time this isn't allowed.
Replace existing call_rcu() based destruction with rcu_work api.
Also allows to undo split of set destruction and gc work cancelling in
a future patch.
Signed-off-by: Florian Westphal <fw@strlen.de>
Signed-off-by: Pablo Neira Ayuso <pablo@netfilter.org>
---
include/linux/netfilter/ipset/ip_set.h | 4 +--
net/netfilter/ipset/ip_set_core.c | 46 ++++++++++++++++----------
2 files changed, 31 insertions(+), 19 deletions(-)
diff --git a/include/linux/netfilter/ipset/ip_set.h b/include/linux/netfilter/ipset/ip_set.h
index cadae9b2578f..c46864cc6623 100644
--- a/include/linux/netfilter/ipset/ip_set.h
+++ b/include/linux/netfilter/ipset/ip_set.h
@@ -244,8 +244,8 @@ extern void ip_set_type_unregister(struct ip_set_type *set_type);
/* A generic IP set */
struct ip_set {
- /* For call_cru in destroy */
- struct rcu_head rcu;
+ /* for set destruction */
+ struct rcu_work rwork;
/* The name of the set */
char name[IPSET_MAXNAMELEN];
/* Lock protecting the set data */
diff --git a/net/netfilter/ipset/ip_set_core.c b/net/netfilter/ipset/ip_set_core.c
index 822a53a7f502..543851a923d0 100644
--- a/net/netfilter/ipset/ip_set_core.c
+++ b/net/netfilter/ipset/ip_set_core.c
@@ -25,6 +25,7 @@
static LIST_HEAD(ip_set_type_list); /* all registered set types */
static DEFINE_MUTEX(ip_set_type_mutex); /* protects ip_set_type_list */
static DEFINE_RWLOCK(ip_set_ref_lock); /* protects the set refs */
+static struct workqueue_struct *ipset_destroy_wq;
struct ip_set_net {
struct ip_set * __rcu *ip_set_list; /* all individual sets */
@@ -1178,22 +1179,26 @@ ip_set_setname_policy[IPSET_ATTR_CMD_MAX + 1] = {
.len = IPSET_MAXNAMELEN - 1 },
};
-/* In order to return quickly when destroying a single set, it is split
- * into two stages:
- * - Cancel garbage collector
- * - Destroy the set itself via call_rcu()
- */
-
static void
-ip_set_destroy_set_rcu(struct rcu_head *head)
+destroy_and_free_set(struct ip_set *set)
{
- struct ip_set *set = container_of(head, struct ip_set, rcu);
-
set->variant->destroy(set);
module_put(set->type->me);
kfree(set);
}
+/* In order to return quickly when destroying a single set,
+ * destruction is done asynchronously via work queues.
+ */
+static void
+ip_set_destroy_set_work(struct work_struct *work)
+{
+ struct ip_set *set = container_of(to_rcu_work(work),
+ struct ip_set, rwork);
+
+ destroy_and_free_set(set);
+}
+
static void
_destroy_all_sets(struct ip_set_net *inst)
{
@@ -1283,7 +1288,8 @@ static int ip_set_destroy(struct sk_buff *skb, const struct nfnl_info *info,
/* Must wait for flush to be really finished */
rcu_barrier();
}
- call_rcu(&s->rcu, ip_set_destroy_set_rcu);
+ INIT_RCU_WORK(&s->rwork, ip_set_destroy_set_work);
+ queue_rcu_work(ipset_destroy_wq, &s->rwork);
}
return 0;
out:
@@ -2421,18 +2427,23 @@ static struct pernet_operations ip_set_net_ops = {
static int __init
ip_set_init(void)
{
- int ret = register_pernet_subsys(&ip_set_net_ops);
+ int ret;
+
+ ipset_destroy_wq = alloc_ordered_workqueue("ipset_destroy_wq", 0);
+ if (!ipset_destroy_wq)
+ return -ENOMEM;
+ ret = register_pernet_subsys(&ip_set_net_ops);
if (ret) {
pr_err("ip_set: cannot register pernet_subsys.\n");
- return ret;
+ goto out_wq;
}
ret = nfnetlink_subsys_register(&ip_set_netlink_subsys);
if (ret != 0) {
pr_err("ip_set: cannot register with nfnetlink.\n");
unregister_pernet_subsys(&ip_set_net_ops);
- return ret;
+ goto out_wq;
}
ret = nf_register_sockopt(&so_set);
@@ -2440,10 +2451,13 @@ ip_set_init(void)
pr_err("SO_SET registry failed: %d\n", ret);
nfnetlink_subsys_unregister(&ip_set_netlink_subsys);
unregister_pernet_subsys(&ip_set_net_ops);
- return ret;
+ goto out_wq;
}
return 0;
+out_wq:
+ destroy_workqueue(ipset_destroy_wq);
+ return ret;
}
static void __exit
@@ -2453,9 +2467,7 @@ ip_set_fini(void)
nfnetlink_subsys_unregister(&ip_set_netlink_subsys);
unregister_pernet_subsys(&ip_set_net_ops);
- /* Wait for call_rcu() in destroy */
- rcu_barrier();
-
+ destroy_workqueue(ipset_destroy_wq);
pr_debug("these are the famous last words\n");
}
--
2.47.3
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH net 08/10] ipvs: avoid out-of-bounds write in ip_vs_nat_icmp
2026-07-31 15:17 [PATCH net 00/10] Netfilter/IPVS fixes for net Pablo Neira Ayuso
` (6 preceding siblings ...)
2026-07-31 15:18 ` [PATCH net 07/10] netfilter: ipset: switch to rcu work Pablo Neira Ayuso
@ 2026-07-31 15:18 ` Pablo Neira Ayuso
2026-07-31 15:18 ` [PATCH net 09/10] ipvs: return the csum validation for forward hook Pablo Neira Ayuso
2026-07-31 15:18 ` [PATCH net 10/10] netfilter: nft_ct: move custom expectation support to helper Pablo Neira Ayuso
9 siblings, 0 replies; 11+ messages in thread
From: Pablo Neira Ayuso @ 2026-07-31 15:18 UTC (permalink / raw)
To: netfilter-devel; +Cc: davem, netdev, kuba, pabeni, edumazet, fw, horms
From: Julian Anastasov <ja@ssi.bg>
Sashiko warns that local attacker can modify the packet
while it is processed by IPVS. Some places read the
IP ihl field multiple times which can cause out-of-bounds
access. One such place is ip_vs_nat_icmp where we
can write after the validated area.
Fix it by providing ciph argument just like it is done for
IPv6 and use ciph->len as offset to the embedded transport
header.
Modify some IPv4 header checks by reading the ihl field
only once.
Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
Link: https://sashiko.dev/#/patchset/20260722101517.36313-1-ja%40ssi.bg
Signed-off-by: Julian Anastasov <ja@ssi.bg>
Signed-off-by: Pablo Neira Ayuso <pablo@netfilter.org>
---
include/net/ip_vs.h | 2 +-
net/netfilter/ipvs/ip_vs_core.c | 67 +++++++++++++++++----------------
net/netfilter/ipvs/ip_vs_xmit.c | 2 +-
3 files changed, 36 insertions(+), 35 deletions(-)
diff --git a/include/net/ip_vs.h b/include/net/ip_vs.h
index e6ca930a3507..1235f1934e94 100644
--- a/include/net/ip_vs.h
+++ b/include/net/ip_vs.h
@@ -2062,7 +2062,7 @@ static inline bool ip_vs_conn_use_hash2(struct ip_vs_conn *cp)
void ip_vs_nat_icmp(struct sk_buff *skb, struct ip_vs_protocol *pp,
struct ip_vs_conn *cp, int dir, unsigned int toff,
- bool has_ports);
+ bool has_ports, struct ip_vs_iphdr *ciph);
#ifdef CONFIG_IP_VS_IPV6
void ip_vs_nat_icmp_v6(struct sk_buff *skb, struct ip_vs_protocol *pp,
diff --git a/net/netfilter/ipvs/ip_vs_core.c b/net/netfilter/ipvs/ip_vs_core.c
index 6b79e0c4d9e2..0bdaeb4ed61e 100644
--- a/net/netfilter/ipvs/ip_vs_core.c
+++ b/net/netfilter/ipvs/ip_vs_core.c
@@ -925,28 +925,27 @@ static int ip_vs_route_me_harder(struct netns_ipvs *ipvs, int af,
*/
void ip_vs_nat_icmp(struct sk_buff *skb, struct ip_vs_protocol *pp,
struct ip_vs_conn *cp, int inout, unsigned int toff,
- bool has_ports)
+ bool has_ports, struct ip_vs_iphdr *ciph)
{
struct iphdr *iph = ip_hdr(skb);
struct icmphdr *icmph = (struct icmphdr *)(skb->data + toff);
- struct iphdr *ciph = (struct iphdr *)(icmph + 1);
- unsigned int coff __maybe_unused = toff + sizeof(struct icmphdr);
+ struct iphdr *cih = (struct iphdr *)(icmph + 1);
if (inout) {
iph->saddr = cp->vaddr.ip;
ip_send_check(iph);
- ciph->daddr = cp->vaddr.ip;
- ip_send_check(ciph);
+ cih->daddr = cp->vaddr.ip;
+ ip_send_check(cih);
} else {
iph->daddr = cp->daddr.ip;
ip_send_check(iph);
- ciph->saddr = cp->daddr.ip;
- ip_send_check(ciph);
+ cih->saddr = cp->daddr.ip;
+ ip_send_check(cih);
}
/* the TCP/UDP/SCTP port */
if (has_ports) {
- __be16 *ports = (void *)ciph + ciph->ihl*4;
+ __be16 *ports = (void *)(skb->data + ciph->len);
if (inout)
ports[1] = cp->vport;
@@ -960,10 +959,10 @@ void ip_vs_nat_icmp(struct sk_buff *skb, struct ip_vs_protocol *pp,
skb->ip_summed = CHECKSUM_UNNECESSARY;
if (inout)
- IP_VS_DBG_PKT(11, AF_INET, pp, skb, coff,
+ IP_VS_DBG_PKT(11, AF_INET, pp, skb, ciph->off,
"Forwarding altered outgoing ICMP");
else
- IP_VS_DBG_PKT(11, AF_INET, pp, skb, coff,
+ IP_VS_DBG_PKT(11, AF_INET, pp, skb, ciph->off,
"Forwarding altered incoming ICMP");
}
@@ -1056,7 +1055,7 @@ static int handle_response_icmp(int af, struct sk_buff *skb,
ip_vs_nat_icmp_v6(skb, pp, cp, 1, toff, has_ports, ciph);
else
#endif
- ip_vs_nat_icmp(skb, pp, cp, 1, toff, has_ports);
+ ip_vs_nat_icmp(skb, pp, cp, 1, toff, has_ports, ciph);
if (ip_vs_route_me_harder(cp->ipvs, af, skb, hooknum))
goto out;
@@ -1092,7 +1091,7 @@ static int ip_vs_out_icmp(struct netns_ipvs *ipvs, struct sk_buff *skb,
struct ip_vs_iphdr ciph;
struct ip_vs_conn *cp;
struct ip_vs_protocol *pp;
- unsigned int offset, ihl;
+ unsigned int offset;
union nf_inet_addr snet;
*related = 1;
@@ -1105,7 +1104,6 @@ static int ip_vs_out_icmp(struct netns_ipvs *ipvs, struct sk_buff *skb,
return NF_ACCEPT;
}
- ihl = ipvsh->len;
offset = ipvsh->len;
ic = skb_header_pointer(skb, offset, sizeof(_icmph), &_icmph);
if (ic == NULL)
@@ -1131,11 +1129,15 @@ static int ip_vs_out_icmp(struct netns_ipvs *ipvs, struct sk_buff *skb,
/* Now find the contained IP header */
offset += sizeof(_icmph);
+ if (!ip_vs_fill_iph_skb_icmp(AF_INET, skb, offset, true, &ciph))
+ return NF_ACCEPT; /* The packet looks wrong, ignore */
+
cih = skb_header_pointer(skb, offset, sizeof(_ciph), &_ciph);
- if (!(cih && cih->version == 4 && cih->ihl >= 5))
+ if (!(cih && cih->version == 4 &&
+ ciph.len - ciph.off >= sizeof(struct iphdr)))
return NF_ACCEPT; /* The packet looks wrong, ignore */
- pp = ip_vs_proto_get(cih->protocol);
+ pp = ip_vs_proto_get(ciph.protocol);
if (!pp)
return NF_ACCEPT;
@@ -1146,8 +1148,6 @@ static int ip_vs_out_icmp(struct netns_ipvs *ipvs, struct sk_buff *skb,
IP_VS_DBG_PKT(11, AF_INET, pp, skb, offset,
"Checking outgoing ICMP for");
- ip_vs_fill_iph_skb_icmp(AF_INET, skb, offset, true, &ciph);
-
/* The embedded headers contain source and dest in reverse order */
cp = INDIRECT_CALL_1(pp->conn_out_get, ip_vs_conn_out_get_proto,
ipvs, AF_INET, skb, &ciph);
@@ -1155,8 +1155,8 @@ static int ip_vs_out_icmp(struct netns_ipvs *ipvs, struct sk_buff *skb,
return NF_ACCEPT;
snet.ip = ipvsh->saddr.ip;
- return handle_response_icmp(AF_INET, skb, &snet, cp, pp, &ciph, ihl,
- hooknum);
+ return handle_response_icmp(AF_INET, skb, &snet, cp, pp, &ciph,
+ ipvsh->len, hooknum);
}
#ifdef CONFIG_IP_VS_IPV6
@@ -1803,10 +1803,12 @@ ip_vs_in_icmp(struct netns_ipvs *ipvs, struct sk_buff *skb, int *related,
/* Now find the contained IP header */
offset += sizeof(_icmph);
cih = skb_header_pointer(skb, offset, sizeof(_ciph), &_ciph);
- if (!(cih && cih->version == 4 && cih->ihl >= 5))
+ if (!cih)
return NF_ACCEPT; /* The packet looks wrong, ignore */
- raddr = (union nf_inet_addr *)&cih->daddr;
hlen_ipip = cih->ihl * 4;
+ if (!(cih->version == 4 && hlen_ipip >= sizeof(struct iphdr)))
+ return NF_ACCEPT; /* The packet looks wrong, ignore */
+ raddr = (union nf_inet_addr *)&cih->daddr;
/* Special case for errors for IPIP/UDP/GRE tunnel packets */
tunnel = false;
@@ -1823,9 +1825,6 @@ ip_vs_in_icmp(struct netns_ipvs *ipvs, struct sk_buff *skb, int *related,
if (!dest || dest->tun_type != IP_VS_CONN_F_TUNNEL_TYPE_IPIP)
return NF_ACCEPT;
offset += hlen_ipip;
- cih = skb_header_pointer(skb, offset, sizeof(_ciph), &_ciph);
- if (!(cih && cih->version == 4 && cih->ihl >= 5))
- return NF_ACCEPT; /* The packet looks wrong, ignore */
tunnel = true;
} else if ((cih->protocol == IPPROTO_UDP || /* Can be UDP encap */
cih->protocol == IPPROTO_GRE) && /* Can be GRE encap */
@@ -1850,21 +1849,25 @@ ip_vs_in_icmp(struct netns_ipvs *ipvs, struct sk_buff *skb, int *related,
/* Skip IP and UDP/GRE tunnel headers */
offset = offset2 + ulen;
/* Now we should be at the original IP header */
- cih = skb_header_pointer(skb, offset, sizeof(_ciph),
- &_ciph);
- if (cih && cih->version == 4 && cih->ihl >= 5 &&
- iproto == IPPROTO_IPIP)
+ if (iproto == IPPROTO_IPIP)
tunnel = true;
else
return NF_ACCEPT;
}
}
- pd = ip_vs_proto_data_get(ipvs, cih->protocol);
+ if (!ip_vs_fill_iph_skb_icmp(AF_INET, skb, offset, !tunnel, &ciph))
+ return NF_ACCEPT;
+ pd = ip_vs_proto_data_get(ipvs, ciph.protocol);
if (!pd)
return NF_ACCEPT;
pp = pd->pp;
+ cih = skb_header_pointer(skb, offset, sizeof(_ciph), &_ciph);
+ if (!(cih && cih->version == 4 &&
+ ciph.len - ciph.off >= sizeof(struct iphdr)))
+ return NF_ACCEPT; /* The packet looks wrong, ignore */
+
/* Is the embedded protocol header present? */
if (unlikely(cih->frag_off & htons(IP_OFFSET) && !pp->dont_defrag))
return NF_ACCEPT;
@@ -1872,9 +1875,6 @@ ip_vs_in_icmp(struct netns_ipvs *ipvs, struct sk_buff *skb, int *related,
IP_VS_DBG_PKT(11, AF_INET, pp, skb, offset,
"Checking incoming ICMP for");
- offset2 = offset;
- ip_vs_fill_iph_skb_icmp(AF_INET, skb, offset, !tunnel, &ciph);
-
/* The embedded headers contain source and dest in reverse order.
* For IPIP/UDP/GRE tunnel this is error for request, not for reply.
*/
@@ -1904,11 +1904,12 @@ ip_vs_in_icmp(struct netns_ipvs *ipvs, struct sk_buff *skb, int *related,
}
if (tunnel) {
- unsigned int hlen_orig = cih->ihl * 4;
+ unsigned int hlen_orig = ciph.len - ciph.off;
__be32 info = ic->un.gateway;
__u8 type = ic->type;
__u8 code = ic->code;
+ offset2 = offset;
/* Update the MTU */
if (ic->type == ICMP_DEST_UNREACH &&
ic->code == ICMP_FRAG_NEEDED) {
diff --git a/net/netfilter/ipvs/ip_vs_xmit.c b/net/netfilter/ipvs/ip_vs_xmit.c
index 0b0c5304993a..c4508f3f43dd 100644
--- a/net/netfilter/ipvs/ip_vs_xmit.c
+++ b/net/netfilter/ipvs/ip_vs_xmit.c
@@ -1580,7 +1580,7 @@ ip_vs_icmp_xmit(struct sk_buff *skb, struct ip_vs_conn *cp,
if (skb_cow(skb, rt->dst.dev->hard_header_len))
goto tx_error;
- ip_vs_nat_icmp(skb, pp, cp, 0, toff, has_ports);
+ ip_vs_nat_icmp(skb, pp, cp, 0, toff, has_ports, ciph);
/* Another hack: avoid icmp_send in ip_fragment */
skb->ignore_df = 1;
--
2.47.3
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH net 09/10] ipvs: return the csum validation for forward hook
2026-07-31 15:17 [PATCH net 00/10] Netfilter/IPVS fixes for net Pablo Neira Ayuso
` (7 preceding siblings ...)
2026-07-31 15:18 ` [PATCH net 08/10] ipvs: avoid out-of-bounds write in ip_vs_nat_icmp Pablo Neira Ayuso
@ 2026-07-31 15:18 ` Pablo Neira Ayuso
2026-07-31 15:18 ` [PATCH net 10/10] netfilter: nft_ct: move custom expectation support to helper Pablo Neira Ayuso
9 siblings, 0 replies; 11+ messages in thread
From: Pablo Neira Ayuso @ 2026-07-31 15:18 UTC (permalink / raw)
To: netfilter-devel; +Cc: davem, netdev, kuba, pabeni, edumazet, fw, horms
From: Julian Anastasov <ja@ssi.bg>
Sashiko notes that playing games with the skb dst and rt
flags instead of providing hooknum is not a good idea
when validating the checksums.
Also, skipping checksum validation for FORWARD packets
risk silent data corruption, even if the only user is
the FTP-CMD packets coming from the real server.
Sashiko also noticed that by using common checksum
helper in the previous commit we actually fixed old bug
where the TCP/UDP checksum for IPv6 on CHECKSUM_COMPLETE
was not validated correctly.
Fixes: e876b75b9020 ("ipvs: fix the checksum validations")
Link: https://sashiko.dev/#/patchset/20260722211420.153933-1-pablo%40netfilter.org
Link: https://sashiko.dev/#/patchset/20260727185024.67534-1-ja%40ssi.bg
Link: https://sashiko.dev/#/patchset/20260728202520.59179-1-ja%40ssi.bg
Signed-off-by: Julian Anastasov <ja@ssi.bg>
Signed-off-by: Pablo Neira Ayuso <pablo@netfilter.org>
---
include/net/ip_vs.h | 19 +++++--------------
net/netfilter/ipvs/ip_vs_proto_sctp.c | 2 +-
2 files changed, 6 insertions(+), 15 deletions(-)
diff --git a/include/net/ip_vs.h b/include/net/ip_vs.h
index 1235f1934e94..d2813eb795be 100644
--- a/include/net/ip_vs.h
+++ b/include/net/ip_vs.h
@@ -25,9 +25,7 @@
#include <linux/netfilter.h> /* for union nf_inet_addr */
#include <linux/ip.h>
#include <linux/ipv6.h> /* for struct ipv6hdr */
-#include <net/route.h>
#include <net/ipv6.h>
-#include <net/ip6_fib.h>
#if IS_ENABLED(CONFIG_NF_CONNTRACK)
#include <net/netfilter/nf_conntrack.h>
#endif
@@ -2095,30 +2093,23 @@ static inline __wsum ip_vs_check_diff2(__be16 old, __be16 new, __wsum oldsum)
return csum_partial(diff, sizeof(diff), oldsum);
}
-static inline bool ip_vs_checksum_needed(struct sk_buff *skb, int af)
+static inline bool ip_vs_checksum_needed(struct sk_buff *skb)
{
/* Checksum unnecessary or already validated? */
if (skb_csum_unnecessary(skb))
return false;
- /* LOCAL_OUT ? */
- if (!skb->dev || skb->dev->flags & IFF_LOOPBACK)
+ /* Locally generated ? */
+ if (!skb->dev)
return false;
- /* !LOCAL_IN (FORWARD) ? */
- if (af == AF_INET6) {
- if (!(dst_rt6_info(skb_dst(skb))->rt6i_flags & RTF_LOCAL))
- return false;
- } else {
- if (!(skb_rtable(skb)->rt_flags & RTCF_LOCAL))
- return false;
- }
return true;
}
static inline bool ip_vs_checksum_common_check(struct sk_buff *skb,
int offset, int proto, int af)
{
- if (!ip_vs_checksum_needed(skb, af))
+ if (!ip_vs_checksum_needed(skb))
return true;
+ /* Validate csum even for FORWARD */
return !nf_checksum(skb, NF_INET_LOCAL_IN, offset, proto, af);
}
diff --git a/net/netfilter/ipvs/ip_vs_proto_sctp.c b/net/netfilter/ipvs/ip_vs_proto_sctp.c
index 3dbd3096e163..c80567c73469 100644
--- a/net/netfilter/ipvs/ip_vs_proto_sctp.c
+++ b/net/netfilter/ipvs/ip_vs_proto_sctp.c
@@ -193,7 +193,7 @@ sctp_csum_check(int af, struct sk_buff *skb, struct ip_vs_protocol *pp,
struct sctphdr *sh;
__le32 cmp, val;
- if (!ip_vs_checksum_needed(skb, af))
+ if (!ip_vs_checksum_needed(skb))
return 1;
sh = (struct sctphdr *)(skb->data + sctphoff);
cmp = sh->checksum;
--
2.47.3
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH net 10/10] netfilter: nft_ct: move custom expectation support to helper
2026-07-31 15:17 [PATCH net 00/10] Netfilter/IPVS fixes for net Pablo Neira Ayuso
` (8 preceding siblings ...)
2026-07-31 15:18 ` [PATCH net 09/10] ipvs: return the csum validation for forward hook Pablo Neira Ayuso
@ 2026-07-31 15:18 ` Pablo Neira Ayuso
9 siblings, 0 replies; 11+ messages in thread
From: Pablo Neira Ayuso @ 2026-07-31 15:18 UTC (permalink / raw)
To: netfilter-devel; +Cc: davem, netdev, kuba, pabeni, edumazet, fw, horms
Originally, the ct expectation support called nf_ct_helper_ext_add() for
confirmed conntracks, which is invalid, triggering a splat. This was
fixed by commit 1710eb913bdc ("netfilter: nft_ct: skip expectations for
confirmed conntrack") which restricted it to confirmed conntracks.
However, early insertion of expectations into the expectations list when
the conntrack is unconfirmed leads to stale entries pointing to the
wrong hlist_head through .pprev due to ct extension reallocation.
Commit 7c9664351980 ("netfilter: move nat hlist_head to nf_conn") moved
the nat hlist_head to nf_conn for this reason:
1. ...
2. When reallocation of extension area occurs we need to fixup the
bysource hash head via hlist_replace_rcu.
But I'd rather not increase the size of the struct nf_conn for this
feature, it only supports for creating expectations in the other
direction and it was broken with DNAT too.
The existing feature has very limited scope because of a pre-existing
issue: two different connections can create the same expectation leading
to expect_clash(), resulting in packet drops.
To address this issue, add an internal ct helper and attach it to the
conntrack entry to streamline the custom ct expectation support with
existing ct helpers.
Expose a new nf_conntrack_helper_free() function to safely release the
internal helper that is allocated and attached to the conntrack entry to
create the custom expectations.
Fixes: 857b46027d6f ("netfilter: nft_ct: add ct expectations support")
Reported-by: Jaeyeong Lee <iostreampy@proton.me>
Link: https://patch.msgid.link/20260715144755.00ea7dfcd9f@proton.me
Signed-off-by: Pablo Neira Ayuso <pablo@netfilter.org>
---
include/net/netfilter/nf_conntrack_helper.h | 2 +
net/netfilter/nf_conntrack_helper.c | 18 ++-
net/netfilter/nft_ct.c | 127 +++++++++++++++-----
3 files changed, 114 insertions(+), 33 deletions(-)
diff --git a/include/net/netfilter/nf_conntrack_helper.h b/include/net/netfilter/nf_conntrack_helper.h
index c761cd8158b2..4420846e41f2 100644
--- a/include/net/netfilter/nf_conntrack_helper.h
+++ b/include/net/netfilter/nf_conntrack_helper.h
@@ -114,6 +114,8 @@ int nf_conntrack_helpers_register(struct nf_conntrack_helper *, unsigned int,
void nf_conntrack_helpers_unregister(struct nf_conntrack_helper **,
unsigned int);
+void nf_conntrack_helper_free(struct nf_conntrack_helper *me);
+
#define nf_conntrack_helper_deprecated(name) \
pr_warn("The %s conntrack helper is scheduled for removal.\n" \
"Please contact the netfilter-devel mailing list if you still need this.\n", name)
diff --git a/net/netfilter/nf_conntrack_helper.c b/net/netfilter/nf_conntrack_helper.c
index 500509b17663..1197e8793494 100644
--- a/net/netfilter/nf_conntrack_helper.c
+++ b/net/netfilter/nf_conntrack_helper.c
@@ -456,13 +456,8 @@ static bool expect_iter_me(struct nf_conntrack_expect *exp, void *data)
return this == me;
}
-void nf_conntrack_helper_unregister(struct nf_conntrack_helper *me)
+void nf_conntrack_helper_free(struct nf_conntrack_helper *me)
{
- mutex_lock(&nf_ct_helper_mutex);
- hlist_del_rcu(&me->hnode);
- nf_ct_helper_count--;
- mutex_unlock(&nf_ct_helper_mutex);
-
/* This helper is going away, disable it. */
rcu_assign_pointer(me->help, NULL);
@@ -476,6 +471,17 @@ void nf_conntrack_helper_unregister(struct nf_conntrack_helper *me)
if (refcount_dec_and_test(&me->ct_refcnt))
kfree_rcu(me, rcu);
}
+EXPORT_SYMBOL_GPL(nf_conntrack_helper_free);
+
+void nf_conntrack_helper_unregister(struct nf_conntrack_helper *me)
+{
+ mutex_lock(&nf_ct_helper_mutex);
+ hlist_del_rcu(&me->hnode);
+ nf_ct_helper_count--;
+ mutex_unlock(&nf_ct_helper_mutex);
+
+ nf_conntrack_helper_free(me);
+}
EXPORT_SYMBOL_GPL(nf_conntrack_helper_unregister);
void nf_ct_helper_init(struct nf_conntrack_helper *helper,
diff --git a/net/netfilter/nft_ct.c b/net/netfilter/nft_ct.c
index 03a88c77e0f0..30c9358dbf48 100644
--- a/net/netfilter/nft_ct.c
+++ b/net/netfilter/nft_ct.c
@@ -1213,6 +1213,8 @@ struct nft_ct_expect_obj {
u8 l4proto;
u8 size;
u32 timeout;
+
+ struct nf_conntrack_helper *helper;
};
static int nft_ct_expect_timeout_get(const struct nlattr *attr, u32 *val)
@@ -1226,6 +1228,73 @@ static int nft_ct_expect_timeout_get(const struct nlattr *attr, u32 *val)
return 0;
}
+struct nft_ct_expect_data {
+ struct nft_ct_expect_obj obj;
+ enum ip_conntrack_dir dir;
+ atomic_t num_expects;
+};
+
+static int ct_expect_help(struct sk_buff *skb, unsigned int protoff,
+ struct nf_conn *ct, enum ip_conntrack_info ctinfo)
+{
+ enum ip_conntrack_dir dir = CTINFO2DIR(ctinfo);
+ struct nft_ct_expect_data *expect_data;
+ struct nf_conntrack_expect *exp;
+ int ret = NF_ACCEPT;
+
+ expect_data = nfct_help_data(ct);
+ if (!expect_data)
+ return NF_ACCEPT;
+
+ if (expect_data->dir != dir)
+ return NF_ACCEPT;
+
+ if (!atomic_add_unless(&expect_data->num_expects, 1, expect_data->obj.size))
+ return NF_ACCEPT;
+
+ exp = nf_ct_expect_alloc(ct);
+ if (!exp) {
+ atomic_dec(&expect_data->num_expects);
+ return NF_DROP;
+ }
+
+ nf_ct_expect_init(exp, NF_CT_EXPECT_CLASS_DEFAULT, nf_ct_l3num(ct),
+ &ct->tuplehash[!dir].tuple.src.u3,
+ &ct->tuplehash[!dir].tuple.dst.u3,
+ expect_data->obj.l4proto, NULL, &expect_data->obj.dport);
+ exp->timeout += expect_data->obj.timeout;
+
+ if (nf_ct_expect_related(exp, 0) != 0) {
+ atomic_dec(&expect_data->num_expects);
+ ret = NF_DROP;
+ }
+
+ nf_ct_expect_put(exp);
+
+ return ret;
+}
+
+static int nft_ct_expect_helper_alloc(struct nft_ct_expect_obj *priv)
+{
+ struct nf_conntrack_helper *ct_expect_helper;
+
+ ct_expect_helper = kzalloc_obj(struct nf_conntrack_helper);
+ if (!ct_expect_helper)
+ return -ENOMEM;
+
+ snprintf(ct_expect_helper->name, sizeof(ct_expect_helper->name), "%s",
+ "nft_ct_expect");
+ ct_expect_helper->me = THIS_MODULE;
+ ct_expect_helper->expect_policy[NF_CT_EXPECT_CLASS_DEFAULT].max_expected = priv->size;
+ rcu_assign_pointer(ct_expect_helper->help, ct_expect_help);
+ refcount_set(&ct_expect_helper->ct_refcnt, 1);
+
+ /* No need to register this helper, this is internal. */
+ priv->helper = ct_expect_helper;
+
+ return 0;
+}
+
static int nft_ct_expect_obj_init(const struct nft_ctx *ctx,
const struct nlattr * const tb[],
struct nft_object *obj)
@@ -1233,6 +1302,8 @@ static int nft_ct_expect_obj_init(const struct nft_ctx *ctx,
struct nft_ct_expect_obj *priv = nft_obj_data(obj);
int err;
+ NF_CT_HELPER_BUILD_BUG_ON(sizeof(struct nft_ct_expect_data));
+
if (!tb[NFTA_CT_EXPECT_L4PROTO] ||
!tb[NFTA_CT_EXPECT_DPORT] ||
!tb[NFTA_CT_EXPECT_TIMEOUT] ||
@@ -1273,13 +1344,26 @@ static int nft_ct_expect_obj_init(const struct nft_ctx *ctx,
priv->dport = nla_get_be16(tb[NFTA_CT_EXPECT_DPORT]);
priv->size = nla_get_u8(tb[NFTA_CT_EXPECT_SIZE]);
- return nf_ct_netns_get(ctx->net, ctx->family);
+ err = nf_ct_netns_get(ctx->net, ctx->family);
+ if (err < 0)
+ return err;
+
+ err = nft_ct_expect_helper_alloc(priv);
+ if (err < 0) {
+ nf_ct_netns_put(ctx->net, ctx->family);
+ return err;
+ }
+
+ return err;
}
static void nft_ct_expect_obj_destroy(const struct nft_ctx *ctx,
- struct nft_object *obj)
+ struct nft_object *obj)
{
+ const struct nft_ct_expect_obj *priv = nft_obj_data(obj);
+
nf_ct_netns_put(ctx->net, ctx->family);
+ nf_conntrack_helper_free(priv->helper);
}
static int nft_ct_expect_obj_dump(struct sk_buff *skb,
@@ -1302,50 +1386,39 @@ static void nft_ct_expect_obj_eval(struct nft_object *obj,
const struct nft_pktinfo *pkt)
{
const struct nft_ct_expect_obj *priv = nft_obj_data(obj);
- struct nf_conntrack_expect *exp;
+ struct nft_ct_expect_data *expect_data;
enum ip_conntrack_info ctinfo;
struct nf_conn_help *help;
- enum ip_conntrack_dir dir;
- u16 l3num = priv->l3num;
struct nf_conn *ct;
ct = nf_ct_get(pkt->skb, &ctinfo);
- if (!ct || nf_ct_is_confirmed(ct) || nf_ct_is_template(ct)) {
+ if (!ct || nf_ct_is_template(ct) || nf_ct_is_confirmed(ct)) {
regs->verdict.code = NFT_BREAK;
return;
}
- dir = CTINFO2DIR(ctinfo);
help = nfct_help(ct);
- if (!help)
- help = nf_ct_helper_ext_add(ct, GFP_ATOMIC);
- if (!help) {
- regs->verdict.code = NF_DROP;
- return;
- }
-
- if (help->expecting[NF_CT_EXPECT_CLASS_DEFAULT] >= priv->size) {
+ if (help) {
regs->verdict.code = NFT_BREAK;
return;
}
- if (l3num == NFPROTO_INET)
- l3num = nf_ct_l3num(ct);
- exp = nf_ct_expect_alloc(ct);
- if (exp == NULL) {
+ help = nf_ct_helper_ext_add(ct, GFP_ATOMIC);
+ if (!help) {
regs->verdict.code = NF_DROP;
return;
}
- nf_ct_expect_init(exp, NF_CT_EXPECT_CLASS_DEFAULT, l3num,
- &ct->tuplehash[!dir].tuple.src.u3,
- &ct->tuplehash[!dir].tuple.dst.u3,
- priv->l4proto, NULL, &priv->dport);
- exp->timeout += priv->timeout;
- if (nf_ct_expect_related(exp, 0) != 0)
- regs->verdict.code = NF_DROP;
+ expect_data = nfct_help_data(ct);
+ if (!expect_data) {
+ regs->verdict.code = NFT_BREAK;
+ return;
+ }
+ expect_data->obj = *priv;
+ expect_data->dir = CTINFO2DIR(ctinfo);
- nf_ct_expect_put(exp);
+ if (help && refcount_inc_not_zero(&priv->helper->ct_refcnt))
+ rcu_assign_pointer(help->helper, priv->helper);
}
static const struct nla_policy nft_ct_expect_policy[NFTA_CT_EXPECT_MAX + 1] = {
--
2.47.3
^ permalink raw reply related [flat|nested] 11+ messages in thread