Re: [PATCH net v4 1/7] net: ip_gre: require CAP_NET_ADMIN in the device netns for changelink

From: Kuniyuki Iwashima

Date: Thu Jun 11 2026 - 01:25:57 EST


On Tue, Jun 9, 2026 at 9:31 AM Maoyi Xie <maoyixie.tju@xxxxxxxxx> wrote:
>
> A tunnel changelink rewrites the tunnel in its creation netns. After an
> IFLA_NET_NS_FD migration that netns is not the caller's. The rtnl
> changelink path only checks CAP_NET_ADMIN against the caller's netns. A
> caller with caps only in its current netns can then rewrite a tunnel
> that lives in another netns, and it picks the endpoint addresses.

nit: This paragraph is not precise (e.g. even without netns migration
a device can use two netns w/ IFLA_LINK_NETNSID, "current netns"
sounds like current->nsproxy->net_ns but not w/ IFLA_NET_NS_PID
etc, also I don't get the last sentence after "it picks...").

Simply state that changelink() operates on at most two netns,
dev_net() and link_net.

>
> Add net_admin_capable(). It requires CAP_NET_ADMIN in the tunnel's netns
> and is skipped when that netns is the device's current netns, where the
> rtnl path already checked the cap. The other patches in this series use
> the same helper.
>
> Gate ipgre_changelink() and erspan_changelink() with it. The check is at
> the top of the op, before any attribute is parsed, because the parsers
> update live tunnel fields first. ipgre_netlink_parms() sets
> t->collect_md before ip_tunnel_changelink() runs.
>
> Commit 8b484efd5cb4 ("ip6: vti: Use ip6_tnl.net in
> vti6_siocdevprivate().") added the same check on the ioctl path. This
> adds it on RTM_NEWLINK.
>
> Reported-by: Xiao Liang <shaw.leon@xxxxxxxxx>
> Closes: https://lore.kernel.org/netdev/CABAhCOSzP1vaThGV35_VnsRCb=87_CPjPVsTHbq905k8A+BuUg@xxxxxxxxxxxxxx/
> Fixes: d0f418516022 ("net, ip_tunnel: fix namespaces move")
> Cc: stable@xxxxxxxxxxxxxxx
> Signed-off-by: Maoyi Xie <maoyixie.tju@xxxxxxxxx>
> ---
> include/net/net_namespace.h | 18 ++++++++++++++++++
> net/ipv4/ip_gre.c | 6 ++++++
> 2 files changed, 24 insertions(+)
>
> diff --git a/include/net/net_namespace.h b/include/net/net_namespace.h
> index 80de5e98a66d..17fb71a78cb6 100644
> --- a/include/net/net_namespace.h
> +++ b/include/net/net_namespace.h

net/core/rtnetlink.c is a better fit.
It has rtnl_get_net_ns_capable().


> @@ -358,6 +358,24 @@ static inline bool net_initialized(const struct net *net)
> return READ_ONCE(net->list.next);
> }
>
> +/**
> + * net_admin_capable - test for CAP_NET_ADMIN over a network namespace
> + * @net: namespace whose state the operation would change
> + * @cur: namespace the operation runs in, e.g. dev_net(dev)
> + *
> + * Returns true when @net is @cur, where CAP_NET_ADMIN was already
> + * checked for the running namespace,
> or when the caller holds
> + * CAP_NET_ADMIN over @net. rtnl changelink paths use this: a device can
> + * be moved so its state lives in a namespace other than the one the
> + * request runs in, and the cap must then be held over that namespace.
> + */
> +static inline bool net_admin_capable(const struct net *net,
> + const struct net *cur)

Rename the helper and change args to

rtnl_dev_link_net_capable(const struct net_device *dev, const struct
net *link_net)

since the netns we care about here is rtnl_newlink_params.link_net
(if specified) and dev_net() is redundant in all callers.

Also remove kdoc, it just reiterates the two conditions below.


> +{
> + return net_eq(net, cur) ||
> + ns_capable(net->user_ns, CAP_NET_ADMIN);
> +}
> +
> static inline void __netns_tracker_alloc(struct net *net,
> netns_tracker *tracker,
> bool refcounted,
> diff --git a/net/ipv4/ip_gre.c b/net/ipv4/ip_gre.c
> index 169e2921a851..040a0ef95184 100644
> --- a/net/ipv4/ip_gre.c
> +++ b/net/ipv4/ip_gre.c
> @@ -1457,6 +1457,9 @@ static int ipgre_changelink(struct net_device *dev, struct nlattr *tb[],
> __u32 fwmark = t->fwmark;
> int err;
>
> + if (!net_admin_capable(t->net, dev_net(dev)))
> + return -EPERM;
> +
> err = ipgre_newlink_encap_setup(dev, data);
> if (err)
> return err;
> @@ -1486,6 +1489,9 @@ static int erspan_changelink(struct net_device *dev, struct nlattr *tb[],
> __u32 fwmark = t->fwmark;
> int err;
>
> + if (!net_admin_capable(t->net, dev_net(dev)))
> + return -EPERM;
> +
> err = ipgre_newlink_encap_setup(dev, data);
> if (err)
> return err;
> --
> 2.34.1
>