* [PATCH net v4 0/2] genetlink: fix joining multicast groups no family owns
@ 2026-11-02 15:58 Anil Kaushik
2026-11-02 15:58 ` [PATCH net v4 1/2] genetlink: Reject " Anil Kaushik
2026-11-02 15:58 ` [PATCH net v4 2/2] netlink: Check a group again if it was released while joining it Anil Kaushik
0 siblings, 2 replies; 9+ messages in thread
From: Anil Kaushik @ 2026-11-02 15:58 UTC (permalink / raw)
To: netdev
Cc: David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, Ido Schimmel, Nicolas Dichtel, Jacob Keller,
Jiri Pirko, Florian Westphal, Mat Martineau, XingWang Xiang,
Alice Ryhl, linux-kernel, Anil Kaushik
genl_bind() checks the GENL_MCAST_CAP_* flags of a group, but the
membership bit is set later, after the lock is dropped. A socket can end
up a member of a group whose flags were never checked for it: when it
joins a group ID that no family owns, or when the family of the group is
unregistered between the check and the point where the bit is set.
Patch 1 rejects joins to group IDs that no family owns. Patch 2 counts,
per group, how many times its members were cleared, which genetlink does
when a family is unregistered. If the count changed after the check, the
group is checked again against its current owner, after the bind
callback is undone if that owner is the one it reached. The core cannot
always tell, see patch 2.
Tested as an unprivileged user, with an msleep() added after the bind
callback in netlink_setsockopt() and netlink_bind(), and a test family
that registers again with GENL_MCAST_CAP_NET_ADMIN:
patch 1 only v4
setsockopt:
unregister in the window,
register later member not member
unregister and register in the window member not member
another family unregistered
(must still join) member member
bind():
unregister and register in the window member not member
With the msleep() before the bind callback instead, and a test family
that registers again without the flag, the family is bound once, and not
twice as with v3:
v3 v4
binds while a member, setsockopt/bind 2 1
binds once the socket is closed 1 0
The test family counts its bind and unbind callbacks: they stay
balanced in all these cases. If the family is replaced just after the
bind callback returned, and before the second read, with an msleep()
there, it gets one unbind too many. The socket is still rejected or
checked against the new family. Normal joins and rtnetlink bind() show
no change.
The test from v1 (join all free IDs, then load gtp and drop_monitor)
gives the same result as before.
I used an LLM to help write and review the code, the tests and the
commit messages of this series. I checked and tested every change
myself.
Changes in v4:
- Patch 2: read the count after ->netlink_bind() too, and call
->netlink_unbind() before checking again if the count had already
changed: ->netlink_bind() can have reached the new owner, which was
then bound twice (netdev Sashiko). The commit message says what the
core still cannot tell.
- Patch 1: no change. It does not close the window on its own, patch 2
does, and it keeps its tags (netdev Sashiko asked about the order).
- The deadlock netdev Sashiko found on the insert failure path of
netlink_bind() exists without this series, it has its own patch:
https://lore.kernel.org/netdev/20261102120929.4077030-1-anilkaushikwireless@gmail.com/
Changes in v3:
- Patch 2: check the group again in af_netlink.c instead of clearing
the members when a family registers. This also covers the case where
the family is unregistered and another one registers, both in the
window, does not walk the sockets on every registration, and does not
touch the members of a group another family still owns (Sashiko).
- Fixes: tag of both patches: the race exists since genl_bind() first
checked a capability, in 4d54cc32112d (Sashiko).
Changes in v2:
- New patch 2 (Sashiko).
- Patch 1: no change, added Reviewed-by tags.
v3: https://lore.kernel.org/netdev/20261101101347.3910654-1-anilkaushikwireless@gmail.com/
v2: https://lore.kernel.org/netdev/20261031054632.3697295-1-anilkaushikwireless@gmail.com/
v1: https://lore.kernel.org/netdev/20261029062214.3407223-1-anilkaushikwireless@gmail.com/
Anil Kaushik (2):
genetlink: Reject joining multicast groups no family owns
netlink: Check a group again if it was released while joining it
net/netlink/af_netlink.c | 79 +++++++++++++++++++++++++++++++++++++++-
net/netlink/af_netlink.h | 1 +
net/netlink/genetlink.c | 3 +-
3 files changed, 81 insertions(+), 2 deletions(-)
--
2.25.1
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH net v4 1/2] genetlink: Reject joining multicast groups no family owns
2026-11-02 15:58 [PATCH net v4 0/2] genetlink: fix joining multicast groups no family owns Anil Kaushik
@ 2026-11-02 15:58 ` Anil Kaushik
2026-11-03 16:00 ` netdev-bot+sashiko
2026-11-02 15:58 ` [PATCH net v4 2/2] netlink: Check a group again if it was released while joining it Anil Kaushik
1 sibling, 1 reply; 9+ messages in thread
From: Anil Kaushik @ 2026-11-02 15:58 UTC (permalink / raw)
To: netdev
Cc: David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, Ido Schimmel, Nicolas Dichtel, Jacob Keller,
Jiri Pirko, Florian Westphal, Mat Martineau, XingWang Xiang,
Alice Ryhl, linux-kernel, Anil Kaushik
genl_bind() checks the GENL_MCAST_CAP_* flags of a group only when a
family owns it. For a group ID that no family owns, it returns 0, so any
user can join it.
A family registered later can get that ID, and the socket stays a member:
nothing checks it again. An unprivileged user can join the free IDs, load
a family module with CTRL_CMD_GETFAMILY, which calls request_module(),
and then receive the notifications of a group with
GENL_MCAST_CAP_NET_ADMIN or GENL_MCAST_CAP_SYS_ADMIN. For example the
drop_monitor "events" group always gets ID 1, and is free until the
drop_monitor module is loaded.
Fix by returning -ENOENT from genl_bind() when no family owns the group.
Nothing is ever sent to such a group, so joining it has no use.
Found by the Sashiko review of the gtp multicast group fix. Tested as an
unprivileged user who joins all free IDs, then loads gtp and
drop_monitor:
Before:
joins: ok=62 EPERM=2
gtp/gtp (ID 15): member
NET_DM/events (ID 1): member
After:
joins: ok=19 ENOENT=43 EPERM=2
gtp/gtp (ID 15): not member
NET_DM/events (ID 1): not member
genl_bind() was added, with its first capability check, in the commit
in the Fixes tag.
Link: https://lore.kernel.org/netdev/179324692107.3134.14443689892607921337@kernel.org/
Fixes: 4d54cc32112d ("mptcp: avoid lock_fast usage in accept path")
Reviewed-by: Nicolas Dichtel <nicolas.dichtel@6wind.com>
Reviewed-by: Ido Schimmel <idosch@nvidia.com>
Signed-off-by: Anil Kaushik <anilkaushikwireless@gmail.com>
---
net/netlink/genetlink.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/net/netlink/genetlink.c b/net/netlink/genetlink.c
index 5cc1037d4917..f038f04ef4ea 100644
--- a/net/netlink/genetlink.c
+++ b/net/netlink/genetlink.c
@@ -1825,7 +1825,7 @@ static int genl_bind(struct net *net, int group)
{
const struct genl_family *family;
unsigned int id;
- int ret = 0;
+ int ret = -ENOENT;
down_read(&cb_lock);
@@ -1841,6 +1841,7 @@ static int genl_bind(struct net *net, int group)
continue;
grp = &family->mcgrps[i];
+ ret = 0;
if ((grp->flags & GENL_MCAST_CAP_NET_ADMIN) &&
!ns_capable(net->user_ns, CAP_NET_ADMIN))
ret = -EPERM;
--
2.25.1
^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH net v4 2/2] netlink: Check a group again if it was released while joining it
2026-11-02 15:58 [PATCH net v4 0/2] genetlink: fix joining multicast groups no family owns Anil Kaushik
2026-11-02 15:58 ` [PATCH net v4 1/2] genetlink: Reject " Anil Kaushik
@ 2026-11-02 15:58 ` Anil Kaushik
2026-11-03 16:00 ` netdev-bot+sashiko
1 sibling, 1 reply; 9+ messages in thread
From: Anil Kaushik @ 2026-11-02 15:58 UTC (permalink / raw)
To: netdev
Cc: David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, Ido Schimmel, Nicolas Dichtel, Jacob Keller,
Jiri Pirko, Florian Westphal, Mat Martineau, XingWang Xiang,
Alice Ryhl, linux-kernel, Anil Kaushik
genl_bind() checks a group under cb_lock, but netlink_setsockopt() and
netlink_bind() set the membership bit only after it returns and the lock
is dropped. If the family is unregistered in between, its members are
cleared before this socket is one. The socket then becomes a member of a
group ID that no family owns, or that a family registered in the
meantime got, and the GENL_MCAST_CAP_* flags of that family are never
checked for it.
Count per group in the netlink table how many times its members were
cleared, which genetlink does when it frees the group IDs of a family.
Read the count before and after ->netlink_bind(), and again once the
table is grabbed. If it changed since the first read, the group was
released, so check it again against its current owner.
If the count changed only after ->netlink_bind() returned, the family
that was bound is gone, so ->netlink_unbind() is not called for it.
Otherwise it is called before the new check, as ->netlink_bind() most
likely reached the new owner. The core cannot tell for sure: if a family
is unregistered and another one registers just after ->netlink_bind()
returned, and before the second read, the new family gets an unbind
without a bind. The check is still done again. In the tree, only the
thermal family has these callbacks, and it is never unregistered.
bind() can then fail after it gave the socket its port ID. This only
happens when a family is unregistered meanwhile.
Found by the Sashiko review of the previous patch, not seen in
production. Triggered in a VM as an unprivileged user, with a test-only
msleep() after the bind callback, and a test family that registers again
with GENL_MCAST_CAP_NET_ADMIN:
Before:
setsockopt() and bind(): the socket is a member of the group
After:
setsockopt() and bind(): -EPERM, not a member
setsockopt(), no family owns the group yet: -ENOENT, not a member
genl_bind() was added, with its first capability check, in the commit
in the Fixes tag.
Link: https://lore.kernel.org/netdev/179340003139.3131.11274717768928642724@kernel.org/
Fixes: 4d54cc32112d ("mptcp: avoid lock_fast usage in accept path")
Signed-off-by: Anil Kaushik <anilkaushikwireless@gmail.com>
---
net/netlink/af_netlink.c | 79 +++++++++++++++++++++++++++++++++++++++-
net/netlink/af_netlink.h | 1 +
2 files changed, 79 insertions(+), 1 deletion(-)
diff --git a/net/netlink/af_netlink.c b/net/netlink/af_netlink.c
index fec2879c497d..96bfebc57899 100644
--- a/net/netlink/af_netlink.c
+++ b/net/netlink/af_netlink.c
@@ -780,6 +780,8 @@ static int netlink_release(struct socket *sock)
old = nl_deref_protected(nl_table[sk->sk_protocol].listeners);
RCU_INIT_POINTER(nl_table[sk->sk_protocol].listeners, NULL);
kfree_rcu(old, rcu);
+ kfree(nl_table[sk->sk_protocol].mc_gen);
+ nl_table[sk->sk_protocol].mc_gen = NULL;
nl_table[sk->sk_protocol].module = NULL;
nl_table[sk->sk_protocol].bind = NULL;
nl_table[sk->sk_protocol].unbind = NULL;
@@ -990,6 +992,9 @@ static int netlink_bind(struct socket *sock, struct sockaddr_unsized *addr,
struct net *net = sock_net(sk);
struct netlink_sock *nlk = nlk_sk(sk);
struct sockaddr_nl *nladdr = (struct sockaddr_nl *)addr;
+ struct netlink_table *tbl = &nl_table[sk->sk_protocol];
+ unsigned int bound_gen[BITS_PER_TYPE(u32)];
+ unsigned int gen[BITS_PER_TYPE(u32)];
int err = 0;
unsigned long groups;
bool bound;
@@ -1023,9 +1028,14 @@ static int netlink_bind(struct socket *sock, struct sockaddr_unsized *addr,
return -EINVAL;
}
+retry:
if (nlk->netlink_bind && groups) {
int group;
+ netlink_lock_table();
+ memcpy(gen, tbl->mc_gen, sizeof(gen));
+ netlink_unlock_table();
+
/* nl_groups is a u32, so cap the maximum groups we can bind */
for (group = 0; group < BITS_PER_TYPE(u32); group++) {
if (!test_bit(group, &groups))
@@ -1036,6 +1046,10 @@ static int netlink_bind(struct socket *sock, struct sockaddr_unsized *addr,
netlink_undo_bind(group, groups, sk);
return err;
}
+
+ netlink_lock_table();
+ memcpy(bound_gen, tbl->mc_gen, sizeof(bound_gen));
+ netlink_unlock_table();
}
/* No need for barriers here as we return to user-space without
@@ -1057,6 +1071,26 @@ static int netlink_bind(struct socket *sock, struct sockaddr_unsized *addr,
netlink_unlock_table();
netlink_table_grab();
+ if (nlk->netlink_bind && groups) {
+ unsigned long gone = 0;
+ bool released = false;
+ int group;
+
+ for_each_set_bit(group, &groups, BITS_PER_TYPE(u32)) {
+ if (gen[group] != tbl->mc_gen[group])
+ released = true;
+ if (bound_gen[group] != tbl->mc_gen[group])
+ __set_bit(group, &gone);
+ }
+ /* see netlink_setsockopt(), undo all but gone and retry all */
+ if (released) {
+ netlink_table_ungrab();
+ netlink_undo_bind(BITS_PER_TYPE(u32), groups & ~gone,
+ sk);
+ bound = true;
+ goto retry;
+ }
+ }
netlink_update_subscriptions(sk, nlk->subscriptions +
hweight32(groups) -
hweight32(nlk->groups[0]));
@@ -1686,6 +1720,8 @@ static int netlink_setsockopt(struct socket *sock, int level, int optname,
break;
case NETLINK_ADD_MEMBERSHIP:
case NETLINK_DROP_MEMBERSHIP: {
+ struct netlink_table *tbl = &nl_table[sk->sk_protocol];
+ unsigned int gen = 0, bound_gen = 0;
int err;
if (!netlink_allowed(sock, NL_CFG_F_NONROOT_RECV))
@@ -1695,12 +1731,34 @@ static int netlink_setsockopt(struct socket *sock, int level, int optname,
return err;
if (!val || val - 1 >= nlk->ngroups)
return -EINVAL;
+retry:
if (optname == NETLINK_ADD_MEMBERSHIP && nlk->netlink_bind) {
+ netlink_lock_table();
+ gen = tbl->mc_gen[val - 1];
+ netlink_unlock_table();
err = nlk->netlink_bind(sock_net(sk), val);
if (err)
return err;
+ netlink_lock_table();
+ bound_gen = tbl->mc_gen[val - 1];
+ netlink_unlock_table();
}
netlink_table_grab();
+ /* The group was released around ->netlink_bind(), and may have
+ * a new owner now, so check it again. If it was released only
+ * after ->netlink_bind() returned, the owner that was bound is
+ * gone: no ->netlink_unbind() for it. Otherwise assume that the
+ * current owner was bound.
+ */
+ if (optname == NETLINK_ADD_MEMBERSHIP && nlk->netlink_bind &&
+ gen != tbl->mc_gen[val - 1]) {
+ bool unbind = bound_gen == tbl->mc_gen[val - 1];
+
+ netlink_table_ungrab();
+ if (unbind && nlk->netlink_unbind)
+ nlk->netlink_unbind(sock_net(sk), val);
+ goto retry;
+ }
netlink_update_socket_mc(nlk, val,
optname == NETLINK_ADD_MEMBERSHIP);
netlink_table_ungrab();
@@ -2031,6 +2089,7 @@ __netlink_kernel_create(struct net *net, int unit, struct module *module,
struct sock *sk;
struct netlink_sock *nlk;
struct listeners *listeners = NULL;
+ unsigned int *mc_gen = NULL;
unsigned int groups;
BUG_ON(!nl_table);
@@ -2052,7 +2111,8 @@ __netlink_kernel_create(struct net *net, int unit, struct module *module,
groups = cfg->groups;
listeners = kzalloc(sizeof(*listeners) + NLGRPSZ(groups), GFP_KERNEL);
- if (!listeners)
+ mc_gen = kcalloc(groups, sizeof(*mc_gen), GFP_KERNEL);
+ if (!listeners || !mc_gen)
goto out_sock_release;
sk->sk_data_ready = netlink_data_ready;
@@ -2069,6 +2129,7 @@ __netlink_kernel_create(struct net *net, int unit, struct module *module,
if (!nl_table[unit].registered) {
nl_table[unit].groups = groups;
rcu_assign_pointer(nl_table[unit].listeners, listeners);
+ nl_table[unit].mc_gen = mc_gen;
nl_table[unit].module = module;
if (cfg) {
nl_table[unit].bind = cfg->bind;
@@ -2079,6 +2140,7 @@ __netlink_kernel_create(struct net *net, int unit, struct module *module,
nl_table[unit].registered = 1;
} else {
kfree(listeners);
+ kfree(mc_gen);
nl_table[unit].registered++;
}
netlink_table_ungrab();
@@ -2086,6 +2148,7 @@ __netlink_kernel_create(struct net *net, int unit, struct module *module,
out_sock_release:
kfree(listeners);
+ kfree(mc_gen);
netlink_kernel_release(sk);
return NULL;
@@ -2113,6 +2176,17 @@ int __netlink_change_ngroups(struct sock *sk, unsigned int groups)
if (groups < 32)
groups = 32;
+ if (tbl->groups < groups) {
+ unsigned int *mc_gen;
+
+ mc_gen = kcalloc(groups, sizeof(*mc_gen), GFP_ATOMIC);
+ if (!mc_gen)
+ return -ENOMEM;
+ memcpy(mc_gen, tbl->mc_gen, tbl->groups * sizeof(*mc_gen));
+ kfree(tbl->mc_gen);
+ tbl->mc_gen = mc_gen;
+ }
+
if (NLGRPSZ(tbl->groups) < NLGRPSZ(groups)) {
new = kzalloc(sizeof(*new) + NLGRPSZ(groups), GFP_ATOMIC);
if (!new)
@@ -2157,6 +2231,9 @@ void __netlink_clear_multicast_users(struct sock *ksk, unsigned int group)
struct netlink_table *tbl = &nl_table[ksk->sk_protocol];
struct hlist_node *tmp;
+ /* the group can be given to another user, see netlink_setsockopt() */
+ tbl->mc_gen[group - 1]++;
+
sk_for_each_bound_safe(sk, tmp, &tbl->mc_list) {
/* bitmap predates the group, can't be a member */
if (group - 1 >= nlk_sk(sk)->ngroups)
diff --git a/net/netlink/af_netlink.h b/net/netlink/af_netlink.h
index 778a3809361f..d5e2e404fd11 100644
--- a/net/netlink/af_netlink.h
+++ b/net/netlink/af_netlink.h
@@ -64,6 +64,7 @@ struct netlink_table {
struct listeners __rcu *listeners;
unsigned int flags;
unsigned int groups;
+ unsigned int *mc_gen;
struct mutex *cb_mutex;
struct module *module;
int (*bind)(struct net *net, int group);
--
2.25.1
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH net v4 1/2] genetlink: Reject joining multicast groups no family owns
2026-11-02 15:58 ` [PATCH net v4 1/2] genetlink: Reject " Anil Kaushik
@ 2026-11-03 16:00 ` netdev-bot+sashiko
2026-11-03 16:12 ` Anil Kaushik
0 siblings, 1 reply; 9+ messages in thread
From: netdev-bot+sashiko @ 2026-11-03 16:00 UTC (permalink / raw)
To: anilkaushikwireless
Cc: netdev, davem, edumazet, kuba, pabeni, horms, idosch,
nicolas.dichtel, jacob.e.keller, jiri, fw, martineau,
v3rdant.xiang, aliceryhl, linux-kernel
Thank you for your contribution! Sashiko AI review found 1 potential
issue(s) to consider.
Critical: 0 · High: 0 · Medium: 1 (1) · Low: 0
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [Medium] In net/netlink/af_netlink.c, netlink_bind() lets one failing
group abort the whole bind().
Pre-existing issues:
- [Medium] Kept only because requires_validation=true.
--
genetlink: Reject joining multicast groups no family owns
genl_bind() now returns -ENOENT when no registered family owns the
requested multicast group ID. Before this change such joins succeeded.
The socket also stayed a member if a family with privileged-only groups
later got the same ID.
Link: https://lore.kernel.org/netdev/179324692107.3134.14443689892607921337@kernel.org/
> diff --git a/net/netlink/genetlink.c b/net/netlink/genetlink.c
> index 5cc1037d4917e..f038f04ef4ead 100644
> --- a/net/netlink/genetlink.c
> +++ b/net/netlink/genetlink.c
> @@ -1825,7 +1825,7 @@ static int genl_bind(struct net *net, int group)
> {
> const struct genl_family *family;
> unsigned int id;
> - int ret = 0;
> + int ret = -ENOENT;
[Severity: Medium]
Can a bind() with an nl_groups mask now fail as a whole when only one
bit in the mask has no owner?
netlink_bind() in net/netlink/af_netlink.c calls the bind hook once for
each set bit. On the first error it undoes the groups it already added
and returns:
net/netlink/af_netlink.c:netlink_bind() {
...
for (group = 0; group < BITS_PER_TYPE(u32); group++) {
if (!test_bit(group, &groups))
continue;
err = nlk->netlink_bind(net, group + 1);
if (!err)
continue;
netlink_undo_bind(group, groups, sk);
return err;
}
...
}
Generic netlink group IDs are sparse. mc_group_start reserves only a few
bits:
static unsigned long mc_group_start = 0x3 | BIT(GENL_ID_CTRL) |
BIT(GENL_ID_VFS_DQUOT) |
BIT(GENL_ID_PMCRAID);
Other IDs are given out only when a family registers. So on a typical
system most IDs in 1..32 have no owner. genl's nlk->ngroups is at least
32, so the masking at the top of netlink_bind() does not remove these
bits.
Before this patch, a bind() on a NETLINK_GENERIC socket with a mask that
mixed owned and unowned IDs (for example nl_groups = ~0) returned 0 and
joined the owned groups.
With this patch, will that call return -ENOENT and join none of the
requested groups, including the valid owned ones?
The netlink core also still treats every ID below ngroups as valid in
netlink_setsockopt():
if (!val || val - 1 >= nlk->ngroups)
return -EINVAL;
genl_bind() now rejects many of those IDs, so the two layers disagree.
The commit message argues that joining an unowned group has no use, and
its test counts single-group joins. It does not mention the bind() mask
case.
The later commit "netlink: Check a group again if it was released while
joining it" keeps both the -ENOENT return and the all-or-nothing loop.
So this behaviour change is still there at the end of the series.
Should the bind() mask path skip unowned IDs instead of failing? If not,
should the commit message mention this uAPI change?
[ ... ]
> @@ -1841,6 +1841,7 @@ static int genl_bind(struct net *net, int group)
> continue;
>
> grp = &family->mcgrps[i];
> + ret = 0;
> if ((grp->flags & GENL_MCAST_CAP_NET_ADMIN) &&
> !ns_capable(net->user_ns, CAP_NET_ADMIN))
> ret = -EPERM;
[Severity: Medium]
This isn't a bug introduced by this patch, but is there still a window
where a socket can become a member of a group ID that was just freed?
genl_bind() checks ownership and the CAP flags under down_read(&cb_lock).
It drops cb_lock before returning. The caller adds the membership later,
under netlink_table_grab():
net/netlink/af_netlink.c:netlink_setsockopt() {
...
if (optname == NETLINK_ADD_MEMBERSHIP && nlk->netlink_bind) {
err = nlk->netlink_bind(sock_net(sk), val);
if (err)
return err;
}
netlink_table_grab();
netlink_update_socket_mc(nlk, val,
optname == NETLINK_ADD_MEMBERSHIP);
...
}
netlink_bind() splits the check and the update in the same way for the
nl_groups mask.
Suppose genl_unregister_family() runs in that gap:
CPU1 CPU2
setsockopt(NETLINK_ADD_MEMBERSHIP)
genl_bind() returns 0
(cb_lock dropped)
genl_unregister_family()
genl_unregister_mc_groups()
__netlink_clear_multicast_users()
clear_bit(grp_id, mc_groups)
netlink_table_grab()
netlink_update_socket_mc()
__netlink_clear_multicast_users() clears only the sockets that are
members at that moment.
Would the socket on CPU1 then be added to the now-free ID?
A family registered later could get that ID, including one whose groups
have GENL_MCAST_CAP_NET_ADMIN or GENL_MCAST_CAP_SYS_ADMIN. It would then
deliver to the socket through do_one_broadcast(), and nothing checks the
membership again.
The commit message says:
"A family registered later can get that ID, and the socket stays a
member: nothing checks it again."
This patch alone does not close that window. The window is narrow,
because a family has to be unregistered during it, for example by rmmod
or by a module init that fails after registration.
The next commit in the series, "netlink: Check a group again if it was
released while joining it", appears to fix this:
- It adds a per-group generation counter, nl_table[].mc_gen, which
__netlink_clear_multicast_users() increments.
- netlink_setsockopt() and netlink_bind() compare the counter again
under netlink_table_grab() and retry if the group was released.
- On retry, genl_bind() returns -ENOENT for the freed ID.
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261102155819.4100930-1-anilkaushikwireless%40gmail.com
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH net v4 2/2] netlink: Check a group again if it was released while joining it
2026-11-02 15:58 ` [PATCH net v4 2/2] netlink: Check a group again if it was released while joining it Anil Kaushik
@ 2026-11-03 16:00 ` netdev-bot+sashiko
2026-11-03 16:13 ` Anil Kaushik
0 siblings, 1 reply; 9+ messages in thread
From: netdev-bot+sashiko @ 2026-11-03 16:00 UTC (permalink / raw)
To: anilkaushikwireless
Cc: netdev, davem, edumazet, kuba, pabeni, horms, idosch,
nicolas.dichtel, jacob.e.keller, jiri, fw, martineau,
v3rdant.xiang, aliceryhl, linux-kernel
Thank you for your contribution! Sashiko AI review found more severe
issue(s) in other patches of this series. This mail is only an FYI, in case
you want to address the nit picks below when you respin.
Low priority / nit picks for your consideration:
- [Low] netlink_add_usersock_entry() (net/netlink/af_netlink.c:2962) sets
up nl_table[NETLINK_USERSOCK]: groups=32, listeners, module,…
- [Low] The kernel-doc of netlink_change_ngroups() says the group count
can be reduced: 'it does not implicitly clear listeners from groups…
- [Low] netlink_setsockopt() (lines 1753-1760) and netlink_bind() (lines
1079-1091) decide whether to call ->netlink_unbind() by comparing…
Pre-existing issues:
- [High] This problem wasn't introduced by this patch, but it is a
deadlock in the code being changed.
--
netlink: Check a group again if it was released while joining it
This patch adds a per-group generation counter (mc_gen) to the netlink
table. __netlink_clear_multicast_users() bumps it when genetlink releases
the group IDs of a family. netlink_bind() and netlink_setsockopt() read
the counter around ->netlink_bind() and again under netlink_table_grab(),
and redo the bind check if the group was released in between.
Link: https://lore.kernel.org/netdev/179340003139.3131.11274717768928642724@kernel.org/
> diff --git a/net/netlink/af_netlink.c b/net/netlink/af_netlink.c
> index 1281c42a3456a..75efc0cfb8052 100644
> --- a/net/netlink/af_netlink.c
> +++ b/net/netlink/af_netlink.c
[ ... ]
> @@ -1036,6 +1046,10 @@ static int netlink_bind(struct socket *sock, struct sockaddr_unsized *addr,
> netlink_undo_bind(group, groups, sk);
> return err;
> }
> +
> + netlink_lock_table();
> + memcpy(bound_gen, tbl->mc_gen, sizeof(bound_gen));
> + netlink_unlock_table();
> }
>
> /* No need for barriers here as we return to user-space without
[Severity: High]
This isn't a bug introduced by this patch, but can the port ID failure
path right after this comment deadlock against genetlink family
registration or unregistration?
net/netlink/af_netlink.c:netlink_bind() {
...
netlink_lock_table();
if (!bound) {
err = nladdr->nl_pid ?
netlink_insert(sk, nladdr->nl_pid) :
netlink_autobind(sock);
if (err) {
netlink_undo_bind(BITS_PER_TYPE(u32), groups, sk);
goto unlock;
}
}
...
}
netlink_undo_bind() runs while this thread still holds an nl_table_users
reference. For NETLINK_GENERIC, nlk->netlink_unbind is genl_unbind(),
which always does down_read(&cb_lock).
genl_register_family() and genl_unregister_family() take cb_lock for
writing through genl_lock_all(). They then call netlink_table_grab() from
genl_validate_assign_mc_groups() or genl_unregister_mc_groups(), and it
sleeps in TASK_UNINTERRUPTIBLE until nl_table_users reaches 0:
CPU1: genl_unregister_family()
genl_lock_all() /* down_write(&cb_lock) */
CPU2: netlink_bind()
netlink_lock_table() /* nl_table_users = 1 */
CPU1: genl_unregister_mc_groups()
netlink_table_grab() /* waits for nl_table_users == 0 */
CPU2: netlink_insert() fails with -EADDRINUSE
netlink_undo_bind()
genl_unbind()
down_read(&cb_lock) /* blocks behind CPU1 */
Neither side can make progress. cb_lock stays write-held, which also
blocks genl_rcv().
An unprivileged user can get to the bind() side. It is enough to bind a
NETLINK_GENERIC socket to a group that has no GENL_MCAST_CAP_* flags,
such as the nlctrl notify group, with an nl_pid that is already in use.
The writer side can be a module unload. It can also be a family
registration triggered through request_module() from CTRL_CMD_GETFAMILY.
The comment in netlink_release() warns about the same lock ordering:
/* must not acquire netlink_table_lock in any way again before unbind
* and notifying genetlink is done as otherwise it might deadlock
*/
The new retry paths in this patch call netlink_table_ungrab() before
calling the unbind callbacks. Could this error path also call
netlink_undo_bind() only after netlink_unlock_table()?
[ ... ]
> @@ -1695,12 +1731,34 @@ static int netlink_setsockopt(struct socket *sock, int level, int optname,
> return err;
> if (!val || val - 1 >= nlk->ngroups)
> return -EINVAL;
> +retry:
> if (optname == NETLINK_ADD_MEMBERSHIP && nlk->netlink_bind) {
> + netlink_lock_table();
> + gen = tbl->mc_gen[val - 1];
> + netlink_unlock_table();
> err = nlk->netlink_bind(sock_net(sk), val);
> if (err)
> return err;
> + netlink_lock_table();
> + bound_gen = tbl->mc_gen[val - 1];
> + netlink_unlock_table();
> }
> netlink_table_grab();
[ ... ]
> + if (optname == NETLINK_ADD_MEMBERSHIP && nlk->netlink_bind &&
> + gen != tbl->mc_gen[val - 1]) {
> + bool unbind = bound_gen == tbl->mc_gen[val - 1];
> +
> + netlink_table_ungrab();
> + if (unbind && nlk->netlink_unbind)
> + nlk->netlink_unbind(sock_net(sk), val);
> + goto retry;
> + }
[Severity: Low]
Can this call ->netlink_unbind() on a family whose ->netlink_bind() never
ran for this socket?
bound_gen is read after genl_bind() has dropped cb_lock. Consider this
order of events, all before the bound_gen read:
- family A is bound
- A is unregistered, and
genl_unregister_mc_groups()->__netlink_clear_multicast_users() bumps
mc_gen
- family B registers and gets the same group ID
In that case gen differs from the current value, but bound_gen matches
it.
genl_unbind() looks up the current owner of the group, so B->unbind() is
called. The retry then calls B->bind(). A later NETLINK_DROP_MEMBERSHIP or
close calls B->unbind() again, so B sees two unbinds for one bind.
netlink_bind() behaves the same way. The group is not set in gone, so it
gets unbound through:
netlink_undo_bind(BITS_PER_TYPE(u32), groups & ~gone, sk);
A family that refcounts in its bind/unbind callbacks would see its count
go below zero.
The commit message documents this limitation: "if a family is
unregistered and another one registers just after ->netlink_bind()
returned, and before the second read, the new family gets an unbind
without a bind". In-tree, only thermal has these callbacks, and it is
never unregistered at runtime.
Is this acceptable for out-of-tree or future families with refcounting
bind/unbind callbacks?
[ ... ]
> @@ -2079,6 +2140,7 @@ __netlink_kernel_create(struct net *net, int unit, struct module *module,
> nl_table[unit].registered = 1;
> } else {
> kfree(listeners);
> + kfree(mc_gen);
> nl_table[unit].registered++;
> }
[Severity: Low]
Does nl_table[NETLINK_USERSOCK] end up with a NULL mc_gen?
netlink_add_usersock_entry() sets groups, listeners, module, flags and
registered = 1 for NETLINK_USERSOCK, but it doesn't allocate mc_gen.
A later kernel socket on NETLINK_USERSOCK takes this else branch, because
registered is already set. It frees its own mc_gen without installing it.
__netlink_change_ngroups() and __netlink_clear_multicast_users() then
dereference tbl->mc_gen without a check:
memcpy(mc_gen, tbl->mc_gen, tbl->groups * sizeof(*mc_gen));
tbl->mc_gen[group - 1]++;
In-tree, both helpers are only called from genetlink with
net->genl_sock. USERSOCK has no ->bind, so the new mc_gen reads in
netlink_bind() and netlink_setsockopt() are not reached for it either.
Should netlink_add_usersock_entry() also allocate mc_gen, so that every
registered table has one?
[ ... ]
> @@ -2113,6 +2176,17 @@ int __netlink_change_ngroups(struct sock *sk, unsigned int groups)
> if (groups < 32)
> groups = 32;
>
> + if (tbl->groups < groups) {
> + unsigned int *mc_gen;
> +
> + mc_gen = kcalloc(groups, sizeof(*mc_gen), GFP_ATOMIC);
> + if (!mc_gen)
> + return -ENOMEM;
> + memcpy(mc_gen, tbl->mc_gen, tbl->groups * sizeof(*mc_gen));
> + kfree(tbl->mc_gen);
> + tbl->mc_gen = mc_gen;
> + }
[Severity: Low]
Does this still work if the number of groups is reduced? The kernel-doc
of netlink_change_ngroups() says:
* change the number of groups to below 32. Also note that it does
* not implicitly clear listeners from groups that are removed when
* the number of groups is reduced.
A shrink lowers tbl->groups and keeps the existing mc_gen. Existing
sockets keep their larger nlk->ngroups, because netlink_realloc_groups()
only grows.
For example, take a table with 64 groups and a socket with
ngroups = 64. If the table shrinks to 32 and then grows to 48, mc_gen
is a 48-entry array and entries 32..47 are reset to 0.
netlink_setsockopt() bounds val only by nlk->ngroups:
if (!val || val - 1 >= nlk->ngroups)
return -EINVAL;
...
gen = tbl->mc_gen[val - 1];
So NETLINK_ADD_MEMBERSHIP with val = 60 would read past the end of
tbl->mc_gen[]. The reset entries could also make a released group look
unchanged, which would skip the new check.
In-tree, genl_validate_assign_mc_groups() is the only caller, and it only
grows the count, so this can't be reached today.
Should mc_gen be sized from the largest group count ever used? The other
option would be to remove the mention of shrinking from the kernel-doc.
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261102155819.4100930-1-anilkaushikwireless%40gmail.com
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH net v4 1/2] genetlink: Reject joining multicast groups no family owns
2026-11-03 16:00 ` netdev-bot+sashiko
@ 2026-11-03 16:12 ` Anil Kaushik
2026-11-03 17:54 ` Nicolas Dichtel
0 siblings, 1 reply; 9+ messages in thread
From: Anil Kaushik @ 2026-11-03 16:12 UTC (permalink / raw)
To: netdev
Cc: netdev-bot+sashiko, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Ido Schimmel,
Nicolas Dichtel, Jacob Keller, Jiri Pirko, Florian Westphal,
Mat Martineau, XingWang Xiang, Alice Ryhl, linux-kernel,
Anil Kaushik
Yes, this is a behaviour change, and I missed it in the commit message.
A bind() mask with an unowned ID now fails with -ENOENT and joins
nothing. I don't know of a program that does this.
Ido, Nicolas, is this OK to keep, or should bind() skip unowned IDs?
I can send a new version if needed.
The other issue, the window before the membership is added, is closed
by patch 2.
Anil
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH net v4 2/2] netlink: Check a group again if it was released while joining it
2026-11-03 16:00 ` netdev-bot+sashiko
@ 2026-11-03 16:13 ` Anil Kaushik
0 siblings, 0 replies; 9+ messages in thread
From: Anil Kaushik @ 2026-11-03 16:13 UTC (permalink / raw)
To: netdev
Cc: netdev-bot+sashiko, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Ido Schimmel,
Nicolas Dichtel, Jacob Keller, Jiri Pirko, Florian Westphal,
Mat Martineau, XingWang Xiang, Alice Ryhl, linux-kernel,
Anil Kaushik
The deadlock is fixed by my separate patch:
https://lore.kernel.org/netdev/20261102120929.4077030-1-anilkaushikwireless@gmail.com/
Anil
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH net v4 1/2] genetlink: Reject joining multicast groups no family owns
2026-11-03 16:12 ` Anil Kaushik
@ 2026-11-03 17:54 ` Nicolas Dichtel
2026-11-03 21:07 ` Jacob Keller
0 siblings, 1 reply; 9+ messages in thread
From: Nicolas Dichtel @ 2026-11-03 17:54 UTC (permalink / raw)
To: Anil Kaushik, netdev
Cc: netdev-bot+sashiko, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Ido Schimmel,
Jacob Keller, Jiri Pirko, Florian Westphal, Mat Martineau,
XingWang Xiang, Alice Ryhl, linux-kernel
Le 03/11/2026 à 17:12, Anil Kaushik a écrit :
> Yes, this is a behaviour change, and I missed it in the commit message.
> A bind() mask with an unowned ID now fails with -ENOENT and joins
> nothing. I don't know of a program that does this.
>
> Ido, Nicolas, is this OK to keep, or should bind() skip unowned IDs?
> I can send a new version if needed.
It's ok for me, but if a regression occurs, a fix will be needed.
I let others speak.
Nicolas
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH net v4 1/2] genetlink: Reject joining multicast groups no family owns
2026-11-03 17:54 ` Nicolas Dichtel
@ 2026-11-03 21:07 ` Jacob Keller
0 siblings, 0 replies; 9+ messages in thread
From: Jacob Keller @ 2026-11-03 21:07 UTC (permalink / raw)
To: nicolas.dichtel, Anil Kaushik, netdev
Cc: netdev-bot+sashiko, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Ido Schimmel,
Jiri Pirko, Florian Westphal, Mat Martineau, XingWang Xiang,
Alice Ryhl, linux-kernel
On 11/3/2026 9:54 AM, Nicolas Dichtel wrote:
> Le 03/11/2026 à 17:12, Anil Kaushik a écrit :
>> Yes, this is a behaviour change, and I missed it in the commit message.
>> A bind() mask with an unowned ID now fails with -ENOENT and joins
>> nothing. I don't know of a program that does this.
>>
>> Ido, Nicolas, is this OK to keep, or should bind() skip unowned IDs?
>> I can send a new version if needed.
> It's ok for me, but if a regression occurs, a fix will be needed.
> I let others speak.
>
> Nicolas
This sounds like it might fall under the "no breaking user space" even
if its quite silly... But I am not an expert here so I am not sure.
Thanks,
Jake
^ permalink raw reply [flat|nested] 9+ messages in thread
end of thread, other threads:[~2026-11-03 21:07 UTC | newest]
Thread overview: 9+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-02 15:58 [PATCH net v4 0/2] genetlink: fix joining multicast groups no family owns Anil Kaushik
2026-11-02 15:58 ` [PATCH net v4 1/2] genetlink: Reject " Anil Kaushik
2026-11-03 16:00 ` netdev-bot+sashiko
2026-11-03 16:12 ` Anil Kaushik
2026-11-03 17:54 ` Nicolas Dichtel
2026-11-03 21:07 ` Jacob Keller
2026-11-02 15:58 ` [PATCH net v4 2/2] netlink: Check a group again if it was released while joining it Anil Kaushik
2026-11-03 16:00 ` netdev-bot+sashiko
2026-11-03 16:13 ` Anil Kaushik
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®