Linux Netfilter development
 help / color / mirror / Atom feed
From: Jakub Kicinski <kuba@kernel.org>
To: pablo@netfilter.org
Cc: Jakub Kicinski <kuba@kernel.org>,
	netfilter-devel@vger.kernel.org, davem@davemloft.net,
	netdev@vger.kernel.org, pabeni@redhat.com, edumazet@google.com,
	horms@kernel.org, fw@strlen.de, ja@ssi.bg
Subject: Re: [PATCH net 06/12] ipvs: bound LBLCR and LBLC cache growth
Date: Thu,  3 Sep 2026 19:01:40 -0700	[thread overview]
Message-ID: <20260904020140.3549818-1-kuba@kernel.org> (raw)
In-Reply-To: <20260903004149.1037028-7-pablo@netfilter.org>

This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
ipvs: bound LBLCR and LBLC cache growth

This adds a hard admission limit to ip_vs_lblc_new() and
ip_vs_lblcr_new(), rejecting new cache entries once atomic_read(&tbl->entries)
reaches max_size * 3 / 2.  The stated intent is to leave headroom so the
periodic collector can catch up while the scheduler fallback keeps serving
traffic.

> diff --git a/net/netfilter/ipvs/ip_vs_lblc.c b/net/netfilter/ipvs/ip_vs_lblc.c
> index 693bcc82ccb7..8180a7ba9f53 100644
> --- a/net/netfilter/ipvs/ip_vs_lblc.c
> +++ b/net/netfilter/ipvs/ip_vs_lblc.c
> @@ -204,6 +204,9 @@ ip_vs_lblc_new(struct ip_vs_lblc_table *tbl, const union nf_inet_addr *daddr,
>  			return en;
>  		ip_vs_lblc_del(en);
>  	}
> +	if (atomic_read(&tbl->entries) >= tbl->max_size * 3 / 2)
> +		return NULL;
> +
>  	en = kmalloc_obj(*en, GFP_ATOMIC);
>  	if (!en)
>  		return NULL;

[Severity: High]
Can tbl->entries be trusted as a bound on the number of live entries in
ip_vs_lblc_new()?  The replacement path just above the new test is the only
ip_vs_lblc_del() call site that does not decrement the counter:

net/netfilter/ipvs/ip_vs_lblc.c:ip_vs_lblc_new() {
	en = ip_vs_lblc_get(af, tbl, daddr);
	if (en) {
		if (en->dest == dest)
			return en;
		ip_vs_lblc_del(en);	/* no atomic_dec(&tbl->entries) */
	}
	...
}

The helper itself does not touch the counter:

static inline void ip_vs_lblc_del(struct ip_vs_lblc_entry *en)
{
	hlist_del_rcu(&en->list);
	call_rcu(&en->rcu_head, ip_vs_lblc_rcu_free);
}

while every other caller pairs it with an explicit decrement, e.g. in
ip_vs_lblc_flush(), ip_vs_lblc_full_check() and ip_vs_lblc_check_expire():

			ip_vs_lblc_del(en);
			atomic_dec(&tbl->entries);

Does that mean each dest change for an already cached daddr adds +1 to
tbl->entries while the live entry count stays the same, since
ip_vs_lblc_hash() is the only place that increments?

	hlist_add_head_rcu(&en->list, &tbl->bucket[hash]);
	atomic_inc(&tbl->entries);

If so, does the drift ever get corrected?  The collector decrements only when
it removes a real entry, so it lowers both the counter and the live count
equally, and tbl->entries otherwise seems to be reset only in
ip_vs_lblc_init_svc().

There also looks to be a second effect from the placement of the new test
after ip_vs_lblc_del(): when the limit fires, the old entry has already been
unlinked and NULL is returned without a decrement and without a replacement,
so the table shrinks while the counter stays where it is.

The replacement path appears to be driven by remote packets, since
ip_vs_lblc_schedule() calls __ip_vs_lblc_schedule() and then
ip_vs_lblc_new() with a different dest whenever the cached dest is not
available, has zero weight, or is_overloaded():

	dest = __ip_vs_lblc_schedule(svc);
	...
	spin_lock_bh(&svc->sched_lock);
	if (!tbl->dead)
		ip_vs_lblc_new(tbl, &iph->daddr, svc->af, dest);

Once the accumulated drift alone reaches max_size * 3 / 2 (24576 with the
default 16384), does ip_vs_lblc_new() return NULL permanently for that
service, even with an empty table?  That would leave LBLC locality caching
disabled for the lifetime of the service, with every packet taking the full
__ip_vs_lblc_schedule() dest walk and ip_vs_lblc_check_expire() still
scanning all 1024 buckets under svc->sched_lock looking for entries that are
no longer there.

That also seems to conflict with the commit message text:

    The extra headroom lets the periodic collector catch up while the
    existing scheduler fallback continues to use the selected destination
    when cache creation fails.

Would adding the missing atomic_dec(&tbl->entries) next to the
ip_vs_lblc_del(en) in ip_vs_lblc_new(), or moving the new test ahead of the
lookup and delete, make the limit match the live entry count?

Note that ip_vs_lblcr_new() below places the identical test inside
if (!en), before any removal, and has no unbalanced delete, so the two
schedulers' counters do not appear to satisfy the same invariant even though
the same expression is applied to both.

> diff --git a/net/netfilter/ipvs/ip_vs_lblcr.c b/net/netfilter/ipvs/ip_vs_lblcr.c
> index f53f05ceea36..858393b1d2d1 100644
> --- a/net/netfilter/ipvs/ip_vs_lblcr.c
> +++ b/net/netfilter/ipvs/ip_vs_lblcr.c
> @@ -363,6 +363,9 @@ ip_vs_lblcr_new(struct ip_vs_lblcr_table *tbl, const union nf_inet_addr *daddr,
>  
>  	en = ip_vs_lblcr_get(af, tbl, daddr);
>  	if (!en) {
> +		if (atomic_read(&tbl->entries) >= tbl->max_size * 3 / 2)
> +			return NULL;
> +
>  		en = kmalloc_obj(*en, GFP_ATOMIC);
>  		if (!en)
>  			return NULL;

[Severity: Medium]
This isn't a bug introduced by this patch, but does the new limit still leave
the per-bucket chain length unbounded?  ip_vs_lblc_hashkey() and
ip_vs_lblcr_hashkey() hash the destination address with no per-table random
seed:

	return hash_32(ntohl(addr_fold), IP_VS_LBLC_TAB_BITS);

so bucket selection for a chosen address is predictable offline, and
ip_vs_lblc_get() / ip_vs_lblcr_get() walk the chain linearly on every
scheduled packet:

	hlist_for_each_entry_rcu(en, &tbl->bucket[hash], list)
		if (ip_vs_addr_equal(af, &en->addr, addr))
			return en;

Since the new test caps only the total entry count and not the chain length,
can all 24576 permitted entries be steered into a single one of the 1024
buckets, leaving the lookup in softirq context (and again under
spin_lock_bh(&svc->sched_lock) from ip_vs_lblc_new()) walking a ~24k element
list per packet?  The patch does reduce the worst case compared to the
unbounded chain before it, so this is a pre-existing exposure rather than
something the patch adds, but would seeding the hash or bounding chain length
be worth doing on top?

  reply	other threads:[~2026-09-04  2:01 UTC|newest]

Thread overview: 23+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-03  0:41 [PATCH net 00/12] Netfilter/IPVS fixes for net Pablo Neira Ayuso
2026-09-03  0:41 ` [PATCH net 01/12] ipvs: reject invalid states in connection template sync records Pablo Neira Ayuso
2026-09-03  0:41 ` [PATCH net 02/12] ipvs: fix reversed sequence option serialization Pablo Neira Ayuso
2026-09-03  0:41 ` [PATCH net 03/12] netfilter: nf_conntrack_sip: fix OOB read in sip_skip_whitespace() Pablo Neira Ayuso
2026-09-03  0:41 ` [PATCH net 04/12] netfilter: cttimeout: prevent UAF during module unload Pablo Neira Ayuso
2026-09-03  0:41 ` [PATCH net 05/12] netfilter: nf_log: unregister loggers before per-net teardown Pablo Neira Ayuso
2026-09-03  0:41 ` [PATCH net 06/12] ipvs: bound LBLCR and LBLC cache growth Pablo Neira Ayuso
2026-09-04  2:01   ` Jakub Kicinski [this message]
2026-09-04  4:20     ` Julian Anastasov
2026-09-03  0:41 ` [PATCH net 07/12] netfilter: nft_payload: restrict checksum offsets to known values Pablo Neira Ayuso
2026-09-04  2:01   ` Jakub Kicinski
2026-09-04  5:56     ` Florian Westphal
2026-09-03  0:41 ` [PATCH net 08/12] netfilter: nfnetlink_log: cope with concurrent instance destruction Pablo Neira Ayuso
2026-09-03  0:41 ` [PATCH net 09/12] netfilter: nfnetlink_queue: hold nfnl mutex in event notifier Pablo Neira Ayuso
2026-09-04  2:01   ` Jakub Kicinski
2026-09-04  5:56     ` Florian Westphal
2026-09-03  0:41 ` [PATCH net 10/12] netfilter: arp_tables: remove the 32bit compat interface Pablo Neira Ayuso
2026-09-03  0:41 ` [PATCH net 11/12] netfilter: ip6_tables: set F_PROTO when proto value is nonzero Pablo Neira Ayuso
2026-09-03  0:41 ` [PATCH net 12/12] netfilter: report NLM_F_DUMP_FILTERED when all is filtered out Pablo Neira Ayuso
2026-09-04  2:04 ` [PATCH net 00/12] Netfilter/IPVS fixes for net Jakub Kicinski
2026-09-04  5:57   ` Florian Westphal
2026-09-04 10:55     ` Pablo Neira Ayuso
2026-09-04 10:59       ` Florian Westphal

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260904020140.3549818-1-kuba@kernel.org \
    --to=kuba@kernel.org \
    --cc=davem@davemloft.net \
    --cc=edumazet@google.com \
    --cc=fw@strlen.de \
    --cc=horms@kernel.org \
    --cc=ja@ssi.bg \
    --cc=netdev@vger.kernel.org \
    --cc=netfilter-devel@vger.kernel.org \
    --cc=pabeni@redhat.com \
    --cc=pablo@netfilter.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox