mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Maoyi Xie <maoyixie.tju@gmail.com>
To: davem@davemloft.net, kuba@kernel.org, edumazet@google.com,
	pabeni@redhat.com
Cc: dsahern@kernel.org, kuniyu@google.com, shaw.leon@gmail.com,
	netdev@vger.kernel.org, linux-kernel@vger.kernel.org,
	stable@vger.kernel.org, Maoyi Xie <maoyixie.tju@gmail.com>
Subject: [PATCH net v3] net: require CAP_NET_ADMIN in the device netns for tunnel changelink
Date: Thu,  4 Jun 2026 20:50:55 +0800	[thread overview]
Message-ID: <20260604125055.3254652-1-maoyixie.tju@gmail.com> (raw)

A tunnel changelink mutates the tunnel state in the device's creation
netns. After an IFLA_NET_NS_FD migration that creation netns differs
from the caller's netns. The rtnl changelink path only checks
CAP_NET_ADMIN against the caller's netns, so a caller with caps only in
its current netns can rewrite a tunnel that lives in the creation netns.
They pick the endpoint addresses. Commit 8b484efd5cb4 ("ip6: vti: Use
ip6_tnl.net in vti6_siocdevprivate().") added the same check on the
ioctl path. This adds it on the RTM_NEWLINK path.

Gate each tunnel changelink on ns_capable against the creation netns, at
the top of the op before any attribute is parsed or applied. The ipv4
types need it there because the parsers can update live tunnel fields
before ip_tunnel_changelink() runs, for example ipgre_netlink_parms()
sets t->collect_md. The check is skipped when the creation netns equals
the device's current netns (net_eq), where the existing CAP_NET_ADMIN
check already applies and no extra LSM hook is wanted.

The newlink path has long checked the capability in the link netns. The
changelink path never did.

Reported-by: Xiao Liang <shaw.leon@gmail.com>
Closes: https://lore.kernel.org/netdev/CABAhCOSzP1vaThGV35_VnsRCb=87_CPjPVsTHbq905k8A+BuUg@mail.gmail.com/
Fixes: d0f418516022 ("net, ip_tunnel: fix namespaces move")
Fixes: 5311a69aaca3 ("net, ip6_tunnel: fix namespaces move")
Fixes: 690afc165bb3 ("net: ip6_gre: fix moving ip6gre between namespaces")
Fixes: f203b76d7809 ("xfrm: Add virtual xfrm interfaces")
Fixes: 11b326fb0a37 ("ip6: vti: Use ip6_tnl.net in vti6_changelink().")
Cc: stable@vger.kernel.org
Signed-off-by: Maoyi Xie <maoyixie.tju@gmail.com>
---
v3: Per Kuniyuki Iwashima's review. Move the check to the top of each
    changelink op, before any attribute is parsed, because the ipv4
    parsers can update live tunnel fields (ipgre_netlink_parms() sets
    t->collect_md) before ip_tunnel_changelink() runs. v2 placed the
    ipv4 check in ip_tunnel_changelink(), which is too late. Also skip
    the check when net_eq(creation netns, dev_net(dev)), to avoid an
    unnecessary LSM invocation when the netns is unchanged. This also
    answers Xiao Liang's question on CSUM and SEQ only changes: with the
    net_eq guard the capability is required only when the device was
    moved to another netns, and then every changelink writes the
    creation netns tunnel regardless of which attribute changed.
v2: Reworked per Kuniyuki's review. v1 gated on
    dev->rtnl_link_ops->get_link_net in __rtnl_newlink(), which is too
    broad. For peer types like netkit and veth get_link_net returns the
    peer netns, which changelink does not mutate. Moved the check into
    each tunnel changelink against the creation netns.

v1 [PATCH net]:
https://lore.kernel.org/netdev/20260527070824.2677331-1-maoyixie.tju@gmail.com/
v2 [PATCH net]:
https://lore.kernel.org/netdev/20260601034148.1272080-1-maoyixie.tju@gmail.com/

 net/ipv4/ip_gre.c              | 8 ++++++++
 net/ipv4/ip_vti.c              | 4 ++++
 net/ipv4/ipip.c                | 4 ++++
 net/ipv6/ip6_gre.c             | 8 ++++++++
 net/ipv6/ip6_tunnel.c          | 4 ++++
 net/ipv6/ip6_vti.c             | 4 ++++
 net/xfrm/xfrm_interface_core.c | 4 ++++
 7 files changed, 36 insertions(+)

diff --git a/net/ipv4/ip_gre.c b/net/ipv4/ip_gre.c
index 169e2921a851..02328c9a3c07 100644
--- a/net/ipv4/ip_gre.c
+++ b/net/ipv4/ip_gre.c
@@ -1457,6 +1457,10 @@ static int ipgre_changelink(struct net_device *dev, struct nlattr *tb[],
 	__u32 fwmark = t->fwmark;
 	int err;
 
+	if (!net_eq(t->net, dev_net(dev)) &&
+	    !ns_capable(t->net->user_ns, CAP_NET_ADMIN))
+		return -EPERM;
+
 	err = ipgre_newlink_encap_setup(dev, data);
 	if (err)
 		return err;
@@ -1486,6 +1490,10 @@ static int erspan_changelink(struct net_device *dev, struct nlattr *tb[],
 	__u32 fwmark = t->fwmark;
 	int err;
 
+	if (!net_eq(t->net, dev_net(dev)) &&
+	    !ns_capable(t->net->user_ns, CAP_NET_ADMIN))
+		return -EPERM;
+
 	err = ipgre_newlink_encap_setup(dev, data);
 	if (err)
 		return err;
diff --git a/net/ipv4/ip_vti.c b/net/ipv4/ip_vti.c
index 95b6bb78fcd2..91c6b2ed7d30 100644
--- a/net/ipv4/ip_vti.c
+++ b/net/ipv4/ip_vti.c
@@ -596,6 +596,10 @@ static int vti_changelink(struct net_device *dev, struct nlattr *tb[],
 	struct ip_tunnel_parm_kern p;
 	__u32 fwmark = t->fwmark;
 
+	if (!net_eq(t->net, dev_net(dev)) &&
+	    !ns_capable(t->net->user_ns, CAP_NET_ADMIN))
+		return -EPERM;
+
 	vti_netlink_parms(data, &p, &fwmark);
 	return ip_tunnel_changelink(dev, tb, &p, fwmark);
 }
diff --git a/net/ipv4/ipip.c b/net/ipv4/ipip.c
index ff95b1b9908e..95976e551bbf 100644
--- a/net/ipv4/ipip.c
+++ b/net/ipv4/ipip.c
@@ -494,6 +494,10 @@ static int ipip_changelink(struct net_device *dev, struct nlattr *tb[],
 	bool collect_md;
 	__u32 fwmark = t->fwmark;
 
+	if (!net_eq(t->net, dev_net(dev)) &&
+	    !ns_capable(t->net->user_ns, CAP_NET_ADMIN))
+		return -EPERM;
+
 	if (ip_tunnel_netlink_encap_parms(data, &ipencap)) {
 		int err = ip_tunnel_encap_setup(t, &ipencap);
 
diff --git a/net/ipv6/ip6_gre.c b/net/ipv6/ip6_gre.c
index 365b4059eb20..7b86deda7e1d 100644
--- a/net/ipv6/ip6_gre.c
+++ b/net/ipv6/ip6_gre.c
@@ -2047,6 +2047,10 @@ static int ip6gre_changelink(struct net_device *dev, struct nlattr *tb[],
 	struct ip6gre_net *ign = net_generic(t->net, ip6gre_net_id);
 	struct __ip6_tnl_parm p;
 
+	if (!net_eq(t->net, dev_net(dev)) &&
+	    !ns_capable(t->net->user_ns, CAP_NET_ADMIN))
+		return -EPERM;
+
 	t = ip6gre_changelink_common(dev, tb, data, &p, extack);
 	if (IS_ERR(t))
 		return PTR_ERR(t);
@@ -2266,6 +2270,10 @@ static int ip6erspan_changelink(struct net_device *dev, struct nlattr *tb[],
 	struct __ip6_tnl_parm p;
 	struct ip6gre_net *ign;
 
+	if (!net_eq(t->net, dev_net(dev)) &&
+	    !ns_capable(t->net->user_ns, CAP_NET_ADMIN))
+		return -EPERM;
+
 	ign = net_generic(t->net, ip6gre_net_id);
 	t = ip6gre_changelink_common(dev, tb, data, &p, extack);
 	if (IS_ERR(t))
diff --git a/net/ipv6/ip6_tunnel.c b/net/ipv6/ip6_tunnel.c
index 9d1037ac082f..dd1458633ec4 100644
--- a/net/ipv6/ip6_tunnel.c
+++ b/net/ipv6/ip6_tunnel.c
@@ -2102,6 +2102,10 @@ static int ip6_tnl_changelink(struct net_device *dev, struct nlattr *tb[],
 	struct ip6_tnl_net *ip6n = net_generic(net, ip6_tnl_net_id);
 	struct ip_tunnel_encap ipencap;
 
+	if (!net_eq(net, dev_net(dev)) &&
+	    !ns_capable(net->user_ns, CAP_NET_ADMIN))
+		return -EPERM;
+
 	if (dev == ip6n->fb_tnl_dev) {
 		if (ip_tunnel_netlink_encap_parms(data, &ipencap)) {
 			/* iproute2 always sets TUNNEL_ENCAP_FLAG_CSUM6, so
diff --git a/net/ipv6/ip6_vti.c b/net/ipv6/ip6_vti.c
index df793c8bfffb..d981ec710f0b 100644
--- a/net/ipv6/ip6_vti.c
+++ b/net/ipv6/ip6_vti.c
@@ -1044,6 +1044,10 @@ static int vti6_changelink(struct net_device *dev, struct nlattr *tb[],
 	struct __ip6_tnl_parm p;
 	struct vti6_net *ip6n;
 
+	if (!net_eq(net, dev_net(dev)) &&
+	    !ns_capable(net->user_ns, CAP_NET_ADMIN))
+		return -EPERM;
+
 	ip6n = net_generic(net, vti6_net_id);
 	if (dev == ip6n->fb_tnl_dev)
 		return -EINVAL;
diff --git a/net/xfrm/xfrm_interface_core.c b/net/xfrm/xfrm_interface_core.c
index 330a05286a56..f11a22edade9 100644
--- a/net/xfrm/xfrm_interface_core.c
+++ b/net/xfrm/xfrm_interface_core.c
@@ -869,6 +869,10 @@ static int xfrmi_changelink(struct net_device *dev, struct nlattr *tb[],
 	struct net *net = xi->net;
 	struct xfrm_if_parms p = {};
 
+	if (!net_eq(net, dev_net(dev)) &&
+	    !ns_capable(net->user_ns, CAP_NET_ADMIN))
+		return -EPERM;
+
 	xfrmi_netlink_parms(data, &p);
 	if (!p.if_id) {
 		NL_SET_ERR_MSG(extack, "if_id must be non zero");

base-commit: 78ef59e7a6459b16f8102e0ee1c718443323d1af
-- 
2.34.1


             reply	other threads:[~2026-06-04 12:51 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-06-04 12:50 Maoyi Xie [this message]
2026-06-09  9:41 ` Paolo Abeni

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=20260604125055.3254652-1-maoyixie.tju@gmail.com \
    --to=maoyixie.tju@gmail.com \
    --cc=davem@davemloft.net \
    --cc=dsahern@kernel.org \
    --cc=edumazet@google.com \
    --cc=kuba@kernel.org \
    --cc=kuniyu@google.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=netdev@vger.kernel.org \
    --cc=pabeni@redhat.com \
    --cc=shaw.leon@gmail.com \
    --cc=stable@vger.kernel.org \
    /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

all inboxes | Powered by JetHome®