[PATCH net] macsec: require CAP_NET_ADMIN in the device netns for changelink

From: Aamir Ahmed

Date: Sat Sep 05 2026 - 17:21:11 EST


macsec_changelink() can reach the driver of macsec->real_dev, but the
rtnl changelink path only checks CAP_NET_ADMIN against dev_net(dev), the
macsec device's own netns. That differs from dev_net(macsec->real_dev)
once the macsec device has been moved into another network namespace,
which is the ordinary way of handing a virtual interface to a container
while the real NIC stays behind.

Two paths in macsec_changelink() cross that boundary:

- IFLA_MACSEC_OFFLOAD -> macsec_update_offload(), which calls
ops->mdo_add_secy() / mdo_del_secy() on macsec->real_dev.
- the tail "propagate to the device" block, which calls
ops->mdo_upd_secy() on macsec->real_dev whenever the device is
already offloaded and any other attribute changed. This one needs
no IFLA_MACSEC_OFFLOAD attribute at all.

So a network namespace administrator with no capability in the real
device's namespace can toggle the real device's MACsec offload state and
push SecY parameters into its driver.

Reproduced with netdevsim, which implements NETIF_F_HW_MACSEC:

# privileged setup in the initial netns
echo "0 1" > /sys/bus/netdevsim/new_device
ip link add link eni0np1 name macsec0 type macsec offload mac
# macsec0 is then handed to a container: moved into an unprivileged
# user+net namespace, while eni0np1 stays in the initial netns.

# from inside that container, with no capability in the initial netns
ip link set macsec0 type macsec offload off # mdo_del_secy
ip link set macsec0 type macsec encrypt off # mdo_upd_secy

Both succeed, and the netdevsim driver in the initial netns logs the
corresponding SecY calls.

Gate the two boundary-crossing paths with rtnl_dev_link_net_capable(),
matching the "require CAP_NET_ADMIN in the device netns for changelink"
series applied to ip_gre, ip6_gre, ipip, ip_vti, ip6_vti, ip6_tunnel,
sit, xfrm_interface, geneve, vxlan and macvlan.

The macsec_is_offloaded() term is required: without it the mdo_upd_secy
path above stays open. The check is deliberately not applied to every
attribute, so that an administrator of the macsec device's own namespace
can still configure a non-offloaded device, whose attributes are local to
it; this follows macvlan, which likewise gates only the settings that
reach the lower device.

Scope: this closes the changelink vector only, which is the sole way to
turn the real device's MACsec offload on or off, and the only way to push
changed SecY parameters to it. Several ndo paths also reach real_dev's
driver as a side effect of the container operating its own device, and
remain gated only by CAP_NET_ADMIN in dev_net(dev), as they do for the
drivers in the series above: macsec_dev_open() / macsec_dev_stop(),
macsec_common_dellink(), and macsec_set_mac_address(), which calls
mdo_upd_secy() because the SCI is derived from the MAC. Those are a
separate question.

Fixes: 3cf3227a21d1 ("net: macsec: hardware offloading infrastructure")
Cc: stable@xxxxxxxxxxxxxxx
Signed-off-by: Aamir Ahmed <elb12345@xxxxxxxxxxxxx>
---
Found and tested with AI assistance (Claude Code). The bug was located by
auditing rtnl_link_ops.changelink handlers for the netns capability check
that ip_gre, ip6_gre, ipip, ip_vti, ip6_vti, ip6_tunnel, sit,
xfrm_interface, geneve, vxlan and macvlan recently gained; macsec and vlan
were the two that did not have it. The reproducer, the fix and the
before/after measurements below were run on a KASAN kernel under virtme-ng
with netdevsim as the offload-capable NIC.

Notes (not part of the commit message):

* Verified on netdevsim across three kernels (vulnerable / unconditional
gate / this patch), with these cases:

offloaded + unpriv cross-netns "offload off" -> denied
offloaded + unpriv cross-netns "encrypt off" -> denied (mdo_upd_secy)
!offloaded + unpriv cross-netns "encrypt off" -> allowed (local only)
!offloaded + unpriv cross-netns "offload mac" -> denied
same-netns privileged "encrypt off" -> allowed (no regression)
privileged cross-netns offload toggle -> allowed (no regression)

An unconditional gate also denies the third case, which is a needless
uAPI change; hence the narrower condition.

* rtnl_dev_link_net_capable() was introduced by the tunnel changelink
series. Older stable trees may need that helper backported first, or
the check open-coded as
net_eq(dev_net(dev), dev_net(macsec->real_dev)) ||
ns_capable(dev_net(macsec->real_dev)->user_ns, CAP_NET_ADMIN)

* VLAN has the same gap and is the last one in this class. Toggling
VLAN_FLAG_GVRP/MVRP runs the GARP/MRP applicant on vlan->real_dev and
transmits PDUs out of it. No driver callback is involved, so the impact
is lower, but there the mutator is also reachable from the SIOCSIFVLAN
SET_VLAN_FLAG_CMD ioctl, so the check has to go in
vlan_dev_change_flags() rather than in the changelink handler. Sent
separately so that the two can be reviewed independently.

drivers/net/macsec.c | 8 ++++++++
1 file changed, 8 insertions(+)

diff --git a/drivers/net/macsec.c b/drivers/net/macsec.c
index 6f9f3aceff..0f351e839f 100644
--- a/drivers/net/macsec.c
+++ b/drivers/net/macsec.c
@@ -19,6 +19,7 @@
#include <net/gro_cells.h>
#include <net/macsec.h>
#include <net/dst_metadata.h>
+#include <net/rtnetlink.h>
#include <net/netdev_lock.h>
#include <linux/phy.h>
#include <linux/byteorder/generic.h>
@@ -3959,6 +3960,13 @@ static int macsec_changelink(struct net_device *dev, struct nlattr *tb[],
if (!data)
return 0;

+ if ((data[IFLA_MACSEC_OFFLOAD] || macsec_is_offloaded(macsec)) &&
+ !rtnl_dev_link_net_capable(dev, dev_net(macsec->real_dev))) {
+ NL_SET_ERR_MSG(extack,
+ "Changing a MACsec device whose real device is in another network namespace requires CAP_NET_ADMIN in that namespace");
+ return -EPERM;
+ }
+
if (data[IFLA_MACSEC_CIPHER_SUITE] ||
data[IFLA_MACSEC_ICV_LEN] ||
data[IFLA_MACSEC_SCI] ||