Merge branch 'net-fix-protodown-with-macvlan'

Ido Schimmel says:

====================
net: Fix protodown with macvlan

When protodown is enabled on a macvlan, two bugs cause the macvlan to
incorrectly gain carrier:

1. Toggling the lower device's carrier while protodown is enabled on the
macvlan causes the macvlan to gain carrier, effectively bypassing the
protodown mechanism.

2. Toggling protodown on and then off on the macvlan while the lower
device has no carrier causes the macvlan to gain carrier, since
netif_change_proto_down() unconditionally turns the carrier on.

Patch #1 is a preparation.

Patch #2 solves the first problem by making netif_carrier_on() return
early when protodown is on.

Patch #3 solves the second problem by only calling netif_carrier_on()
when protodown is turned off if there is no linked net device or if the
linked net device has a carrier.

Patch #4 adds a selftest covering both bugs and the basic protodown
functionality.

Targeting at net-next since these are not regressions (i.e., never
worked).

Note that while these changes are in the core, they should only affect
macvlan as protodown is only supported by macvlan and vxlan and only
the former has a linked net device.
====================

Link: https://patch.msgid.link/20260507105906.891817-1-idosch@nvidia.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
This commit is contained in:
Jakub Kicinski 2026-05-08 16:50:29 -07:00
commit d3f65237fa
4 changed files with 207 additions and 2 deletions

View File

@ -10141,17 +10141,36 @@ bool netdev_port_same_parent_id(struct net_device *a, struct net_device *b)
}
EXPORT_SYMBOL(netdev_port_same_parent_id);
static struct net_device *dev_get_iflink_dev(struct net_device *dev)
{
struct net *net;
ASSERT_RTNL();
if (!dev->netdev_ops->ndo_get_iflink || !dev->rtnl_link_ops ||
!dev->rtnl_link_ops->get_link_net)
return dev;
net = dev->rtnl_link_ops->get_link_net(dev);
return __dev_get_by_index(net, dev_get_iflink(dev));
}
int netif_change_proto_down(struct net_device *dev, bool proto_down)
{
struct net_device *iflink_dev;
if (!dev->change_proto_down)
return -EOPNOTSUPP;
if (!netif_device_present(dev))
return -ENODEV;
iflink_dev = dev_get_iflink_dev(dev);
if (!iflink_dev)
return -ENODEV;
WRITE_ONCE(dev->proto_down, proto_down);
if (proto_down)
netif_carrier_off(dev);
else
else if (dev == iflink_dev || netif_carrier_ok(iflink_dev))
netif_carrier_on(dev);
WRITE_ONCE(dev->proto_down, proto_down);
return 0;
}

View File

@ -609,6 +609,9 @@ static void netdev_watchdog_down(struct net_device *dev)
*/
void netif_carrier_on(struct net_device *dev)
{
if (READ_ONCE(dev->proto_down))
return;
if (test_and_clear_bit(__LINK_STATE_NOCARRIER, &dev->state)) {
if (dev->reg_state == NETREG_UNINITIALIZED)
return;

View File

@ -69,6 +69,7 @@ TEST_PROGS := \
nl_netdev.py \
nl_nlctrl.py \
pmtu.sh \
protodown.sh \
psock_snd.sh \
reuseaddr_ports_exhausted.sh \
reuseport_addr_any.sh \

View File

@ -0,0 +1,182 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
#
# Test the protodown mechanism. Verify basic protodown toggling, protodown
# reasons, operational state when the lower device carrier changes, and correct
# operational state when the lower device has no carrier.
# shellcheck disable=SC1091,SC2034,SC2154,SC2317
source lib.sh
require_command jq
ALL_TESTS="
protodown_basic_macvlan
protodown_basic_vxlan
protodown_reasons
protodown_lower_toggle
protodown_lower_down
"
operstate_get()
{
local ns=$1; shift
local dev=$1; shift
ip -n "$ns" -j link show dev "$dev" | jq -r '.[].operstate'
}
operstate_check()
{
local ns=$1; shift
local dev=$1; shift
local expected=$1; shift
local current
current=$(operstate_get "$ns" "$dev")
[ "$current" = "$expected" ]
}
setup_prepare()
{
setup_ns NS
defer cleanup_all_ns
ip -n "$NS" link add name dummy0 up type dummy
ip -n "$NS" link add name macvlan0 link dummy0 up type macvlan mode bridge
ip -n "$NS" link add name vxlan0 up type vxlan id 10010 dstport 4789
}
protodown_basic()
{
local dev=$1; shift
ip -n "$NS" link set dev "$dev" protodown on
check_err $? "Failed to set protodown on"
busywait "$BUSYWAIT_TIMEOUT" operstate_check "$NS" "$dev" DOWN
check_err $? "Operational state is not DOWN after setting protodown"
ip -n "$NS" link set dev "$dev" protodown off
check_err $? "Failed to set protodown off"
busywait "$BUSYWAIT_TIMEOUT" operstate_check "$NS" "$dev" UP
check_err $? "Operational state is not UP after clearing protodown"
}
protodown_basic_macvlan()
{
RET=0
protodown_basic macvlan0
log_test "Basic protodown on/off with macvlan"
}
protodown_basic_vxlan()
{
RET=0
protodown_basic vxlan0
log_test "Basic protodown on/off with vxlan"
}
protodown_reasons()
{
RET=0
ip -n "$NS" link set dev macvlan0 protodown on
ip -n "$NS" link set dev macvlan0 protodown_reason 0 on
check_err $? "Failed to set protodown reason bit 0"
# Cannot clear protodown while reasons are active.
ip -n "$NS" link set dev macvlan0 protodown off 2>/dev/null
check_fail $? "Clearing protodown succeeded with active reasons"
ip -n "$NS" link set dev macvlan0 protodown_reason 0 off
check_err $? "Failed to clear protodown reason bit 0"
# Can clear protodown when no reasons are active.
ip -n "$NS" link set dev macvlan0 protodown off
check_err $? "Failed to clear protodown with no active reasons"
busywait "$BUSYWAIT_TIMEOUT" operstate_check "$NS" macvlan0 UP
check_err $? "Operational state is not UP after clearing protodown"
log_test "Protodown reasons"
}
protodown_lower_toggle()
{
RET=0
ip -n "$NS" link set dev macvlan0 protodown on
busywait "$BUSYWAIT_TIMEOUT" operstate_check "$NS" macvlan0 DOWN
check_err $? "Operational state is not DOWN after setting protodown"
# Toggle carrier on the lower device. The macvlan should stay DOWN
# because protodown is on.
ip -n "$NS" link set dev dummy0 carrier off
ip -n "$NS" link set dev dummy0 carrier on
busywait "$BUSYWAIT_TIMEOUT" operstate_check "$NS" dummy0 UP
check_err $? "Lower device is not UP after carrier on"
busywait "$BUSYWAIT_TIMEOUT" operstate_check "$NS" macvlan0 DOWN
check_err $? "Macvlan operational state is not DOWN despite protodown"
# Clear protodown and verify the macvlan comes back up.
ip -n "$NS" link set dev macvlan0 protodown off
busywait "$BUSYWAIT_TIMEOUT" operstate_check "$NS" macvlan0 UP
check_err $? "Operational state is not UP after clearing protodown"
log_test "Protodown with lower device toggled"
}
protodown_lower_down()
{
RET=0
# Bring the lower device carrier down first.
ip -n "$NS" link set dev dummy0 carrier off
busywait "$BUSYWAIT_TIMEOUT" operstate_check "$NS" macvlan0 LOWERLAYERDOWN
check_err $? "Macvlan is not LOWERLAYERDOWN with lower carrier off"
# Toggle protodown on and off while lower has no carrier. The macvlan
# should not transition to UP.
ip -n "$NS" link set dev macvlan0 protodown on
busywait "$BUSYWAIT_TIMEOUT" operstate_check "$NS" macvlan0 LOWERLAYERDOWN
check_err $? "Macvlan is not LOWERLAYERDOWN after setting protodown"
ip -n "$NS" link set dev macvlan0 protodown off
busywait "$BUSYWAIT_TIMEOUT" operstate_check "$NS" macvlan0 LOWERLAYERDOWN
check_err $? "Macvlan is not LOWERLAYERDOWN after clearing protodown"
# Bring the lower device carrier up. The macvlan should transition to
# UP.
ip -n "$NS" link set dev dummy0 carrier on
busywait "$BUSYWAIT_TIMEOUT" operstate_check "$NS" dummy0 UP
check_err $? "Lower device is not UP after carrier on"
busywait "$BUSYWAIT_TIMEOUT" operstate_check "$NS" macvlan0 UP
check_err $? "Macvlan is not UP after lower device is UP"
log_test "Protodown with lower device down"
}
trap defer_scopes_cleanup EXIT
setup_prepare
tests_run
exit "$EXIT_STATUS"