From: Jakub Kicinski <kuba@kernel.org>
To: zhilinz@nebusec.ai
Cc: Jakub Kicinski <kuba@kernel.org>,
netdev@vger.kernel.org, dsahern@kernel.org, idosch@nvidia.com,
davem@davemloft.net, edumazet@google.com, pabeni@redhat.com,
horms@kernel.org, vega@nebusec.ai
Subject: Re: [PATCH net 1/1] ipv6: flowlabel: enforce the per-socket lease cap on reused labels
Date: Thu, 6 Aug 2026 08:34:47 -0700 [thread overview]
Message-ID: <20260806153447.2591671-2-kuba@kernel.org> (raw)
In-Reply-To: <cf4fdc79ae4dc46bd4eb7eeb57e5de2091c13cd3.1785746178.git.zhilinz@nebusec.ai>
Please trim the hash in the Fixes tag.
This is an AI-generated review of your patch. The human sending this
email has considered the AI review valid, or at least plausible.
---
ipv6: flowlabel: enforce the per-socket lease cap on reused labels
This adds mem_check_dup() and calls it from the recheck/reuse path in
ipv6_flowlabel_get(), so that re-acquiring an already existing flowlabel
also has to pass the FL_MAX_PER_SOCK per-socket lease check. The
linger/expires refresh of the existing label is made conditional on that
check passing.
A few questions on the changelog and on the new check below.
[Severity: Low]
The changelog says:
"mem_check() only counts socket leases after fl_size falls below
FL_MAX_SIZE - FL_MAX_PER_SOCK. Reusing an existing flowlabel does not
increase fl_size, so duplicate GET requests can keep taking the
recheck path and grow one socket's lease list without ever hitting the
FL_MAX_PER_SOCK limit."
Is that description of the mechanism accurate? Two things look inverted.
First, the early return in mem_check() is:
room = FL_MAX_SIZE - fl_size;
if (room > FL_MAX_SIZE - FL_MAX_PER_SOCK)
return 0;
which is room > 8160, i.e. fl_size < FL_MAX_PER_SOCK (32). So the lease
count is skipped while fl_size is small, not "after fl_size falls below
FL_MAX_SIZE - FL_MAX_PER_SOCK" (8160), which is true nearly all of the
time. Was room confused with fl_size here?
Second, before this patch the recheck block in ipv6_flowlabel_get() had no
mem_check() call at all, so the per-socket cap was not consulted on the
reuse path regardless of fl_size. Should the message describe this as an
unconditional absence of accounting on the reuse path rather than a narrow
fl_size window? That distinction matters for the stable tag, since the
reproducer is just: create a label with share != IPV6_FL_S_EXCL, then
repeatedly call setsockopt(IPV6_FLOWLABEL_MGR, IPV6_FL_A_GET) on it.
> diff --git a/net/ipv6/ip6_flowlabel.c b/net/ipv6/ip6_flowlabel.c
> index 1ab5ad0dcf24f..e295f85bcc604 100644
> --- a/net/ipv6/ip6_flowlabel.c
> +++ b/net/ipv6/ip6_flowlabel.c
> @@ -493,6 +493,24 @@ static int mem_check(struct sock *sk)
> return 0;
> }
>
> +static int mem_check_dup(struct sock *sk)
> +{
> + struct ipv6_fl_socklist *sfl;
> + int count = 0;
> +
> + lockdep_assert_held(&ip6_fl_lock);
> +
> + rcu_read_lock();
> + for_each_sk_fl_rcu(sk, sfl)
> + count++;
> + rcu_read_unlock();
[Severity: Low]
Is ip6_fl_lock the right lock to assert here? The only data
mem_check_dup() reads is the per-socket list inet_sk(sk)->ipv6_fl_list, and
this file documents that list as being guarded by ip6_sk_fl_lock:
#define socklist_dereference(__sflp) \
rcu_dereference_protected(__sflp, lockdep_is_held(&ip6_sk_fl_lock))
and fl_link() inserts under it:
spin_lock_bh(&ip6_sk_fl_lock);
sfl->fl = fl;
sfl->next = inet->ipv6_fl_list;
rcu_assign_pointer(inet->ipv6_fl_list, sfl);
spin_unlock_bh(&ip6_sk_fl_lock);
ip6_fl_lock is described as:
/* FL hash table lock: it protects only of GC */
and guards fl_ht[], fl_size and net->ipv6.flowlabel_count, none of which
mem_check_dup() touches (unlike mem_check(), which reads fl_size). So the
assertion advertises a contract that is unrelated to the data being
walked, and the lock that actually serializes insertion and removal is not
held across the count and the following fl_link().
As far as I can tell there is no actual race today, because
ipv6_flowlabel_get() and ipv6_flowlabel_put() both run under
sockopt_lock_sock(sk) taken in do_ipv6_setsockopt() before
IPV6_FLOWLABEL_MGR is dispatched, and fl6_free_socklist() only runs at
teardown. Would it be clearer to drop the assertion (and the new
spin_lock_bh(&ip6_fl_lock) around the call) and instead note the socket
lock invariant in a comment, or assert ip6_sk_fl_lock if a lock is wanted?
As written, the global ip6_fl_lock is held across an O(n) list walk that
needs none of the state it protects, which contends with
ip6_fl_gc()/fl_intern()/fl_release().
> +
> + if (count >= FL_MAX_PER_SOCK && !capable(CAP_NET_ADMIN))
> + return -ENOBUFS;
> +
> + return 0;
> +}
> +
[Severity: Medium]
The count here is over all leases the socket holds, not over duplicates of
the requested label, and the check omits mem_check()'s room/fl_size and
net->ipv6.flowlabel_count gating. That makes the reuse path strictly
stricter than the create path. Is that intended?
Concretely, while fl_size < FL_MAX_PER_SOCK, mem_check() returns 0 early:
room = FL_MAX_SIZE - fl_size;
if (room > FL_MAX_SIZE - FL_MAX_PER_SOCK)
return 0;
so the same unprivileged socket may still intern brand-new labels past 32
leases, while mem_check_dup() refuses to share an existing one at the same
count. For example: create label L, take 31 further leases of L so
count == 32 with fl_size == 1, then IPV6_FL_F_CREATE a new label — room is
8191 > 8160 and mem_check() allows it, but a plain IPV6_FL_A_GET of an
existing label returns -ENOBUFS.
Should mem_check_dup() count only leases of the requested label, or reuse
the same gating that mem_check() applies?
> static inline void fl_link(struct sock *sk, struct ipv6_fl_socklist *sfl,
> struct ip6_flowlabel *fl)
> {
> @@ -679,10 +697,17 @@ static int ipv6_flowlabel_get(struct sock *sk, struct in6_flowlabel_req *freq,
> err = -ENOMEM;
> if (!sfl1)
> goto release;
> - if (fl->linger > fl1->linger)
> - fl1->linger = fl->linger;
> - if ((long)(fl->expires - fl1->expires) > 0)
> - fl1->expires = fl->expires;
> + spin_lock_bh(&ip6_fl_lock);
> + err = mem_check_dup(sk);
> + if (err == 0) {
> + if (fl->linger > fl1->linger)
> + fl1->linger = fl->linger;
> + if ((long)(fl->expires - fl1->expires) > 0)
> + fl1->expires = fl->expires;
> + }
> + spin_unlock_bh(&ip6_fl_lock);
> + if (err != 0)
> + goto release;
[Severity: Medium]
This block changes user-visible behaviour in ways the changelog does not
mention, since it says only that it keeps "the duplicate-acquisition path
consistent with the long-standing per-socket cap".
An IPV6_FL_A_GET that re-acquires or shares an existing label, which
previously always succeeded, now returns -ENOBUFS once the socket holds 32
leases of any labels.
When the cap fires, the fl1->linger / fl1->expires refresh is skipped, so
a GET-based refresh loop stops refreshing with no error path that
distinguishes it from a full failure. Is IPV6_FL_A_RENEW meant to be the
only remaining way to refresh here? Would it be better to keep the
refresh unconditional and only reject the new fl_link()?
The create branch is also affected. After:
err = mem_check(sk);
if (err == 0)
fl1 = fl_intern(net, fl, freq->flr_label);
...
if (fl1)
goto recheck;
a plain IPV6_FL_A_GET|IPV6_FL_F_CREATE that merely loses a create race
against another socket re-enters this block and is re-adjudicated by the
stricter rule, so it can now fail with -ENOBUFS even though mem_check()
already approved it under ip6_fl_lock. Is that acceptable?
Leases are only dropped one at a time by an explicit IPV6_FL_A_PUT in
ipv6_flowlabel_put(), or at teardown in fl6_free_socklist(), so a socket
that reaches the cap stays capped until it puts labels back.
> fl_link(sk, sfl1, fl1);
> fl_free(fl);
> return 0;
--
pw-bot: cr
prev parent reply other threads:[~2026-08-06 15:35 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-03 11:22 [PATCH net 0/1] ipv6: flowlabel: enforce the per-socket lease cap on reused labels Zhiling Zou
2026-08-03 11:22 ` [PATCH net 1/1] " Zhiling Zou
2026-08-06 15:34 ` Jakub Kicinski [this message]
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=20260806153447.2591671-2-kuba@kernel.org \
--to=kuba@kernel.org \
--cc=davem@davemloft.net \
--cc=dsahern@kernel.org \
--cc=edumazet@google.com \
--cc=horms@kernel.org \
--cc=idosch@nvidia.com \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=vega@nebusec.ai \
--cc=zhilinz@nebusec.ai \
/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