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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.