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(+)
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
© 2016 - 2026 Red Hat, Inc.