[PATCH net v5 1/2] genetlink: Reject joining multicast groups no family owns

From: Anil Kaushik

Date: Wed Nov 04 2026 - 04:22:55 EST


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.

bind() takes a mask of groups, and most IDs in it can have no owner. Such
a mask used to work, so keep it working: netlink_bind() skips an ID when
the bind callback returns -ENOENT, and joins the others. Only genl_bind()
returns -ENOENT. A skipped ID is not joined, so it is no longer in the
groups that getsockname() reports. setsockopt() returns the error, as it
asks for one group.

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
bind(), an owned group and free IDs: ok, member of the owned group only

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@xxxxxxxxxx/
Fixes: 4d54cc32112d ("mptcp: avoid lock_fast usage in accept path")
Signed-off-by: Anil Kaushik <anilkaushikwireless@xxxxxxxxx>
---
net/netlink/af_netlink.c | 6 ++++++
net/netlink/genetlink.c | 3 ++-
2 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/net/netlink/af_netlink.c b/net/netlink/af_netlink.c
index fec2879c497d..94c1390e8dc5 100644
--- a/net/netlink/af_netlink.c
+++ b/net/netlink/af_netlink.c
@@ -1033,6 +1033,12 @@ static int netlink_bind(struct socket *sock, struct sockaddr_unsized *addr,
err = nlk->netlink_bind(net, group + 1);
if (!err)
continue;
+ /* no one owns this group, still join the others */
+ if (err == -ENOENT) {
+ __clear_bit(group, &groups);
+ err = 0;
+ continue;
+ }
netlink_undo_bind(group, groups, sk);
return err;
}
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