--- /dev/null
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Test propagation of a real device's state to the VLANs stacked on top of it
+# when the real device is (or becomes) a bond member.
+#
+# The kernel mirrors a real device's UP/DOWN, MTU and feature changes onto its
+# VLANs. This is done asynchronously (netdev_work): doing it synchronously from
+# the real device's notifier could deadlock. If the real device is brought up
+# while enslaved to a bond - so its instance lock is held across NETDEV_UP - and
+# a VLAN on top of it is itself a bond member, the synchronous propagation
+# re-entered the stack and tried to take the same instance lock again.
+#
+# Cover both halves:
+# - the deferred UP/DOWN, MTU and feature propagation actually lands on the
+# VLAN (link state and MTU use an ops-locked dummy, i.e. the deferral path),
+# - the deadlock-prone topology - a VLAN on a dummy, with the VLAN and the
+# dummy each enslaved to a different bond - can be built without hanging.
+
+ALL_TESTS="
+ vlan_link_state
+ vlan_mtu
+ vlan_features
+ vlan_real_dev_enslave
+"
+
+REQUIRE_MZ=no
+NUM_NETIFS=0
+lib_dir=$(dirname "$0")
+source "$lib_dir"/../../../net/forwarding/lib.sh
+
+# Return 0 if $dev in netns $ns has flag $flag set (e.g. UP) in its <...> flags.
+link_has_flag()
+{
+ local ns=$1 dev=$2 flag=$3
+
+ ip -n "$ns" link show dev "$dev" 2>/dev/null | grep -q "[<,]${flag}[,>]"
+}
+
+link_lacks_flag()
+{
+ ! link_has_flag "$@"
+}
+
+link_mtu_is()
+{
+ local ns=$1 dev=$2 want=$3 cur
+
+ cur=$(ip -n "$ns" link show dev "$dev" 2>/dev/null | \
+ sed -n 's/.* mtu \([0-9]\+\).*/\1/p')
+ [ "$cur" = "$want" ]
+}
+
+vlan_feature_is()
+{
+ local ns=$1 dev=$2 feature=$3 value=$4
+
+ ip netns exec "$ns" ethtool -k "$dev" 2>/dev/null | \
+ grep -q "^$feature: $value"
+}
+
+link_has_master()
+{
+ local ns=$1 dev=$2 master=$3
+
+ ip -n "$ns" -o link show dev "$dev" 2>/dev/null | grep -q "master $master"
+}
+
+vlan_link_state()
+{
+ RET=0
+
+ ip -n "$NS" link add ls_dummy type dummy
+ ip -n "$NS" link add link ls_dummy name ls_vlan type vlan id 100
+
+ # Bringing the real device up must propagate UP to the VLAN.
+ ip -n "$NS" link set ls_dummy up
+ busywait "$BUSYWAIT_TIMEOUT" link_has_flag "$NS" ls_vlan UP
+ check_err $? "VLAN did not go UP after the real device went UP"
+
+ # ... and likewise for DOWN.
+ ip -n "$NS" link set ls_dummy down
+ busywait "$BUSYWAIT_TIMEOUT" link_lacks_flag "$NS" ls_vlan UP
+ check_err $? "VLAN did not go DOWN after the real device went DOWN"
+
+ ip -n "$NS" link del ls_vlan
+ ip -n "$NS" link del ls_dummy
+
+ log_test "VLAN link state follows the real device"
+}
+
+vlan_mtu()
+{
+ RET=0
+
+ # The VLAN inherits the real device's MTU (2000) at creation time.
+ ip -n "$NS" link add mtu_dummy mtu 2000 type dummy
+ ip -n "$NS" link add link mtu_dummy name mtu_vlan type vlan id 100
+
+ # Shrinking the real device's MTU must clamp the VLAN's MTU.
+ ip -n "$NS" link set mtu_dummy mtu 1500
+ busywait "$BUSYWAIT_TIMEOUT" link_mtu_is "$NS" mtu_vlan 1500
+ check_err $? "VLAN MTU not clamped after the real device's MTU shrank"
+
+ ip -n "$NS" link del mtu_vlan
+ ip -n "$NS" link del mtu_dummy
+
+ log_test "VLAN MTU clamped to the real device"
+}
+
+vlan_features()
+{
+ RET=0
+
+ # Use veth as the real device: unlike dummy it exports vlan_features, so
+ # the VLAN actually inherits a toggleable offload to assert on.
+ ip -n "$NS" link add ft_veth type veth peer name ft_veth_pr
+ ip -n "$NS" link add link ft_veth name ft_vlan type vlan id 100
+
+ vlan_feature_is "$NS" ft_vlan scatter-gather on
+ check_err $? "VLAN did not inherit scatter-gather from the real device"
+
+ # Toggling the offload on the real device must propagate to the VLAN.
+ ip netns exec "$NS" ethtool -K ft_veth sg off
+ busywait "$BUSYWAIT_TIMEOUT" \
+ vlan_feature_is "$NS" ft_vlan scatter-gather off
+ check_err $? "VLAN scatter-gather still on after disabling it on real dev"
+
+ ip netns exec "$NS" ethtool -K ft_veth sg on
+ busywait "$BUSYWAIT_TIMEOUT" \
+ vlan_feature_is "$NS" ft_vlan scatter-gather on
+ check_err $? "VLAN scatter-gather still off after enabling it on real dev"
+
+ ip -n "$NS" link del ft_vlan
+ ip -n "$NS" link del ft_veth
+
+ log_test "VLAN features follow the real device"
+}
+
+vlan_real_dev_enslave()
+{
+ RET=0
+
+ # dummy <- VLAN -> bond0, then enslave the dummy itself to bond1. The
+ # last step brings the dummy up under bond1's instance lock, which used
+ # to deadlock while synchronously propagating UP to the (bond-enslaved)
+ # VLAN on top.
+ ip -n "$NS" link add dl_dummy type dummy
+ ip -n "$NS" link set dl_dummy up
+ ip -n "$NS" link add link dl_dummy name dl_vlan type vlan id 100
+
+ ip -n "$NS" link add dl_bond0 type bond mode active-backup
+ ip -n "$NS" link set dl_vlan down
+ ip -n "$NS" link set dl_vlan master dl_bond0
+ check_err $? "could not enslave the VLAN to bond0"
+
+ ip -n "$NS" link add dl_bond1 type bond mode active-backup
+ ip -n "$NS" link set dl_dummy down
+ ip -n "$NS" link set dl_dummy master dl_bond1
+ check_err $? "could not enslave the real device to bond1"
+
+ # If we got here the kernel did not deadlock; make sure it is still
+ # responsive and the enslave really took effect.
+ link_has_master "$NS" dl_dummy dl_bond1
+ check_err $? "real device not enslaved to bond1"
+
+ ip -n "$NS" link del dl_bond1
+ ip -n "$NS" link del dl_bond0
+ ip -n "$NS" link del dl_vlan
+ ip -n "$NS" link del dl_dummy
+
+ log_test "VLAN real device enslaved to a second bond"
+}
+
+setup_ns NS
+trap 'cleanup_ns $NS' EXIT
+
+tests_run
+
+exit "$EXIT_STATUS"