From e1ab601bb23006e2710359c0fba27342f8887aec Mon Sep 17 00:00:00 2001 From: Cosmin Ratiu Date: Wed, 8 Apr 2026 14:52:37 +0300 Subject: selftests: Migrate nsim-only MACsec tests to Python Move MACsec offload API and ethtool feature tests from tools/testing/selftests/drivers/net/netdevsim/macsec-offload.sh to tools/testing/selftests/drivers/net/macsec.py using the NetDrvEnv framework so tests can run against both netdevsim (default) and real hardware (NETIF=ethX). As some real hardware requires MACsec to use encryption, add that to the tests. Netdevsim-specific limit checks (max SecY, max RX SC) were moved into separate test cases to avoid failures on real hardware. Signed-off-by: Cosmin Ratiu Reviewed-by: Sabrina Dubroca Link: https://patch.msgid.link/20260408115240.1636047-2-cratiu@nvidia.com Signed-off-by: Jakub Kicinski --- tools/testing/selftests/drivers/net/Makefile | 1 + tools/testing/selftests/drivers/net/config | 1 + tools/testing/selftests/drivers/net/macsec.py | 202 +++++++++++++++++++++ .../selftests/drivers/net/netdevsim/Makefile | 1 - .../drivers/net/netdevsim/macsec-offload.sh | 117 ------------ 5 files changed, 204 insertions(+), 118 deletions(-) create mode 100755 tools/testing/selftests/drivers/net/macsec.py delete mode 100755 tools/testing/selftests/drivers/net/netdevsim/macsec-offload.sh (limited to 'tools') diff --git a/tools/testing/selftests/drivers/net/Makefile b/tools/testing/selftests/drivers/net/Makefile index 8154d6d429d3..5e045dde0273 100644 --- a/tools/testing/selftests/drivers/net/Makefile +++ b/tools/testing/selftests/drivers/net/Makefile @@ -13,6 +13,7 @@ TEST_GEN_FILES := \ TEST_PROGS := \ gro.py \ hds.py \ + macsec.py \ napi_id.py \ napi_threaded.py \ netpoll_basic.py \ diff --git a/tools/testing/selftests/drivers/net/config b/tools/testing/selftests/drivers/net/config index 77ccf83d87e0..d4b31a317c09 100644 --- a/tools/testing/selftests/drivers/net/config +++ b/tools/testing/selftests/drivers/net/config @@ -3,6 +3,7 @@ CONFIG_DEBUG_INFO_BTF=y CONFIG_DEBUG_INFO_BTF_MODULES=n CONFIG_INET_PSP=y CONFIG_IPV6=y +CONFIG_MACSEC=m CONFIG_NETCONSOLE=m CONFIG_NETCONSOLE_DYNAMIC=y CONFIG_NETCONSOLE_EXTENDED_LOG=y diff --git a/tools/testing/selftests/drivers/net/macsec.py b/tools/testing/selftests/drivers/net/macsec.py new file mode 100755 index 000000000000..5220c0656c0c --- /dev/null +++ b/tools/testing/selftests/drivers/net/macsec.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 + +"""MACsec tests.""" + +import os + +from lib.py import ksft_run, ksft_exit, ksft_eq, ksft_raises +from lib.py import CmdExitFailure, KsftSkipEx +from lib.py import NetDrvEpEnv +from lib.py import cmd, ip, defer, ethtool + +# Unique prefix per run to avoid collisions in the shared netns. +# Keep it short: IFNAMSIZ is 16 (incl. NUL), and VLAN names append ".". +MACSEC_PFX = f"ms{os.getpid()}_" + + +def _macsec_name(idx=0): + return f"{MACSEC_PFX}{idx}" + + +def _get_macsec_offload(dev): + """Returns macsec offload mode string from ip -d link show.""" + info = ip(f"-d link show dev {dev}", json=True)[0] + return info.get("linkinfo", {}).get("info_data", {}).get("offload") + + +def _get_features(dev): + """Returns ethtool features dict for a device.""" + return ethtool(f"-k {dev}", json=True)[0] + + +def _require_ip_macsec(cfg): + """SKIP if iproute2 on local or remote lacks 'ip macsec' support.""" + for host in [None, cfg.remote]: + out = cmd("ip macsec help", fail=False, host=host) + if "Usage" not in out.stdout + out.stderr: + where = "remote" if host else "local" + raise KsftSkipEx(f"iproute2 too old on {where}," + " missing macsec support") + + +def _require_ip_macsec_offload(): + """SKIP if local iproute2 doesn't understand 'ip macsec offload'.""" + out = cmd("ip macsec help", fail=False) + if "offload" not in out.stdout + out.stderr: + raise KsftSkipEx("iproute2 too old, missing macsec offload") + + +def _require_macsec_offload(cfg): + """SKIP if local device doesn't support macsec-hw-offload.""" + _require_ip_macsec_offload() + try: + feat = ethtool(f"-k {cfg.ifname}", json=True)[0] + except (CmdExitFailure, IndexError) as e: + raise KsftSkipEx( + f"can't query features: {e}") from e + if not feat.get("macsec-hw-offload", {}).get("active"): + raise KsftSkipEx("macsec-hw-offload not supported") + + +def test_offload_api(cfg) -> None: + """MACsec offload API: create SecY, add SA/rx, toggle offload.""" + + _require_macsec_offload(cfg) + ms0 = _macsec_name(0) + ms1 = _macsec_name(1) + ms2 = _macsec_name(2) + + # Create 3 SecY with offload + ip(f"link add link {cfg.ifname} {ms0} type macsec " + f"port 4 encrypt on offload mac") + defer(ip, f"link del {ms0}") + + ip(f"link add link {cfg.ifname} {ms1} type macsec " + f"address aa:bb:cc:dd:ee:ff port 5 encrypt on offload mac") + defer(ip, f"link del {ms1}") + + ip(f"link add link {cfg.ifname} {ms2} type macsec " + f"sci abbacdde01020304 encrypt on offload mac") + defer(ip, f"link del {ms2}") + + # Add TX SA + ip(f"macsec add {ms0} tx sa 0 pn 1024 on " + "key 01 12345678901234567890123456789012") + + # Add RX SC + SA + ip(f"macsec add {ms0} rx port 1234 address 1c:ed:de:ad:be:ef") + ip(f"macsec add {ms0} rx port 1234 address 1c:ed:de:ad:be:ef " + "sa 0 pn 1 on key 00 0123456789abcdef0123456789abcdef") + + # Can't disable offload when SAs are configured + with ksft_raises(CmdExitFailure): + ip(f"link set {ms0} type macsec offload off") + with ksft_raises(CmdExitFailure): + ip(f"macsec offload {ms0} off") + + # Toggle offload via rtnetlink on SA-free device + ip(f"link set {ms2} type macsec offload off") + ip(f"link set {ms2} type macsec encrypt on offload mac") + + # Toggle offload via genetlink + ip(f"macsec offload {ms2} off") + ip(f"macsec offload {ms2} mac") + + +def test_max_secy(cfg) -> None: + """nsim-only test for max number of SecYs.""" + + cfg.require_nsim() + _require_ip_macsec_offload() + ms0 = _macsec_name(0) + ms1 = _macsec_name(1) + ms2 = _macsec_name(2) + ms3 = _macsec_name(3) + + ip(f"link add link {cfg.ifname} {ms0} type macsec " + f"port 4 encrypt on offload mac") + defer(ip, f"link del {ms0}") + + ip(f"link add link {cfg.ifname} {ms1} type macsec " + f"address aa:bb:cc:dd:ee:ff port 5 encrypt on offload mac") + defer(ip, f"link del {ms1}") + + ip(f"link add link {cfg.ifname} {ms2} type macsec " + f"sci abbacdde01020304 encrypt on offload mac") + defer(ip, f"link del {ms2}") + with ksft_raises(CmdExitFailure): + ip(f"link add link {cfg.ifname} {ms3} " + f"type macsec port 8 encrypt on offload mac") + + +def test_max_sc(cfg) -> None: + """nsim-only test for max number of SCs.""" + + cfg.require_nsim() + _require_ip_macsec_offload() + ms0 = _macsec_name(0) + + ip(f"link add link {cfg.ifname} {ms0} type macsec " + f"port 4 encrypt on offload mac") + defer(ip, f"link del {ms0}") + ip(f"macsec add {ms0} rx port 1234 address 1c:ed:de:ad:be:ef") + with ksft_raises(CmdExitFailure): + ip(f"macsec add {ms0} rx port 1235 address 1c:ed:de:ad:be:ef") + + +def test_offload_state(cfg) -> None: + """Offload state reflects configuration changes.""" + + _require_macsec_offload(cfg) + ms0 = _macsec_name(0) + + # Create with offload on + ip(f"link add link {cfg.ifname} {ms0} type macsec " + f"encrypt on offload mac") + cleanup = defer(ip, f"link del {ms0}") + + ksft_eq(_get_macsec_offload(ms0), "mac", + "created with offload: should be mac") + feats_on_1 = _get_features(ms0) + + ip(f"link set {ms0} type macsec offload off") + ksft_eq(_get_macsec_offload(ms0), "off", + "offload disabled: should be off") + feats_off_1 = _get_features(ms0) + + ip(f"link set {ms0} type macsec encrypt on offload mac") + ksft_eq(_get_macsec_offload(ms0), "mac", + "offload re-enabled: should be mac") + ksft_eq(_get_features(ms0), feats_on_1, + "features should match first offload-on snapshot") + + # Delete and recreate without offload + cleanup.exec() + ip(f"link add link {cfg.ifname} {ms0} type macsec") + defer(ip, f"link del {ms0}") + ksft_eq(_get_macsec_offload(ms0), "off", + "created without offload: should be off") + ksft_eq(_get_features(ms0), feats_off_1, + "features should match first offload-off snapshot") + + ip(f"link set {ms0} type macsec encrypt on offload mac") + ksft_eq(_get_macsec_offload(ms0), "mac", + "offload enabled after create: should be mac") + ksft_eq(_get_features(ms0), feats_on_1, + "features should match first offload-on snapshot") + + +def main() -> None: + """Main program.""" + with NetDrvEpEnv(__file__) as cfg: + ksft_run([test_offload_api, + test_max_secy, + test_max_sc, + test_offload_state, + ], args=(cfg,)) + ksft_exit() + + +if __name__ == "__main__": + main() diff --git a/tools/testing/selftests/drivers/net/netdevsim/Makefile b/tools/testing/selftests/drivers/net/netdevsim/Makefile index 1a228c5430f5..9808c2fbae9e 100644 --- a/tools/testing/selftests/drivers/net/netdevsim/Makefile +++ b/tools/testing/selftests/drivers/net/netdevsim/Makefile @@ -11,7 +11,6 @@ TEST_PROGS := \ fib.sh \ fib_notifications.sh \ hw_stats_l3.sh \ - macsec-offload.sh \ nexthop.sh \ peer.sh \ psample.sh \ diff --git a/tools/testing/selftests/drivers/net/netdevsim/macsec-offload.sh b/tools/testing/selftests/drivers/net/netdevsim/macsec-offload.sh deleted file mode 100755 index 98033e6667d2..000000000000 --- a/tools/testing/selftests/drivers/net/netdevsim/macsec-offload.sh +++ /dev/null @@ -1,117 +0,0 @@ -#!/bin/bash -# SPDX-License-Identifier: GPL-2.0-only - -source ethtool-common.sh - -NSIM_NETDEV=$(make_netdev) -MACSEC_NETDEV=macsec_nsim - -set -o pipefail - -if ! ethtool -k $NSIM_NETDEV | grep -q 'macsec-hw-offload: on'; then - echo "SKIP: netdevsim doesn't support MACsec offload" - exit 4 -fi - -if ! ip link add link $NSIM_NETDEV $MACSEC_NETDEV type macsec offload mac 2>/dev/null; then - echo "SKIP: couldn't create macsec device" - exit 4 -fi -ip link del $MACSEC_NETDEV - -# -# test macsec offload API -# - -ip link add link $NSIM_NETDEV "${MACSEC_NETDEV}" type macsec port 4 offload mac -check $? - -ip link add link $NSIM_NETDEV "${MACSEC_NETDEV}2" type macsec address "aa:bb:cc:dd:ee:ff" port 5 offload mac -check $? - -ip link add link $NSIM_NETDEV "${MACSEC_NETDEV}3" type macsec sci abbacdde01020304 offload mac -check $? - -ip link add link $NSIM_NETDEV "${MACSEC_NETDEV}4" type macsec port 8 offload mac 2> /dev/null -check $? '' '' 1 - -ip macsec add "${MACSEC_NETDEV}" tx sa 0 pn 1024 on key 01 12345678901234567890123456789012 -check $? - -ip macsec add "${MACSEC_NETDEV}" rx port 1234 address "1c:ed:de:ad:be:ef" -check $? - -ip macsec add "${MACSEC_NETDEV}" rx port 1234 address "1c:ed:de:ad:be:ef" sa 0 pn 1 on \ - key 00 0123456789abcdef0123456789abcdef -check $? - -ip macsec add "${MACSEC_NETDEV}" rx port 1235 address "1c:ed:de:ad:be:ef" 2> /dev/null -check $? '' '' 1 - -# can't disable macsec offload when SAs are configured -ip link set "${MACSEC_NETDEV}" type macsec offload off 2> /dev/null -check $? '' '' 1 - -ip macsec offload "${MACSEC_NETDEV}" off 2> /dev/null -check $? '' '' 1 - -# toggle macsec offload via rtnetlink -ip link set "${MACSEC_NETDEV}2" type macsec offload off -check $? - -ip link set "${MACSEC_NETDEV}2" type macsec offload mac -check $? - -# toggle macsec offload via genetlink -ip macsec offload "${MACSEC_NETDEV}2" off -check $? - -ip macsec offload "${MACSEC_NETDEV}2" mac -check $? - -for dev in ${MACSEC_NETDEV}{,2,3} ; do - ip link del $dev - check $? -done - - -# -# test ethtool features when toggling offload -# - -ip link add link $NSIM_NETDEV $MACSEC_NETDEV type macsec offload mac -TMP_FEATS_ON_1="$(ethtool -k $MACSEC_NETDEV)" - -ip link set $MACSEC_NETDEV type macsec offload off -TMP_FEATS_OFF_1="$(ethtool -k $MACSEC_NETDEV)" - -ip link set $MACSEC_NETDEV type macsec offload mac -TMP_FEATS_ON_2="$(ethtool -k $MACSEC_NETDEV)" - -[ "$TMP_FEATS_ON_1" = "$TMP_FEATS_ON_2" ] -check $? - -ip link del $MACSEC_NETDEV - -ip link add link $NSIM_NETDEV $MACSEC_NETDEV type macsec -check $? - -TMP_FEATS_OFF_2="$(ethtool -k $MACSEC_NETDEV)" -[ "$TMP_FEATS_OFF_1" = "$TMP_FEATS_OFF_2" ] -check $? - -ip link set $MACSEC_NETDEV type macsec offload mac -check $? - -TMP_FEATS_ON_3="$(ethtool -k $MACSEC_NETDEV)" -[ "$TMP_FEATS_ON_1" = "$TMP_FEATS_ON_3" ] -check $? - - -if [ $num_errors -eq 0 ]; then - echo "PASSED all $((num_passes)) checks" - exit 0 -else - echo "FAILED $num_errors/$((num_errors+num_passes)) checks" - exit 1 -fi -- cgit v1.2.3 From 26555673bc7888b80d5867618525eb04d4216a24 Mon Sep 17 00:00:00 2001 From: Cosmin Ratiu Date: Wed, 8 Apr 2026 14:52:39 +0300 Subject: selftests: Add MACsec VLAN propagation traffic test Add VLAN filter propagation tests through offloaded MACsec devices via actual traffic. The tests create MACsec tunnels with matching SAs on both endpoints, stack VLANs on top, and verify connectivity with ping. Covered: - Offloaded MACsec with VLAN (filters propagate to HW) - Software MACsec with VLAN (no HW filter propagation) - Offload on/off toggle and verifying traffic still works On netdevsim this makes use of the VLAN filter debugfs file to actually validate that filters are applied/removed correctly. On real hardware the traffic should validate actual VLAN filter propagation. Signed-off-by: Cosmin Ratiu Reviewed-by: Sabrina Dubroca Link: https://patch.msgid.link/20260408115240.1636047-4-cratiu@nvidia.com Signed-off-by: Jakub Kicinski --- tools/testing/selftests/drivers/net/config | 1 + tools/testing/selftests/drivers/net/lib/py/env.py | 9 ++ tools/testing/selftests/drivers/net/macsec.py | 141 ++++++++++++++++++++++ 3 files changed, 151 insertions(+) (limited to 'tools') diff --git a/tools/testing/selftests/drivers/net/config b/tools/testing/selftests/drivers/net/config index d4b31a317c09..fd16994366f4 100644 --- a/tools/testing/selftests/drivers/net/config +++ b/tools/testing/selftests/drivers/net/config @@ -8,4 +8,5 @@ CONFIG_NETCONSOLE=m CONFIG_NETCONSOLE_DYNAMIC=y CONFIG_NETCONSOLE_EXTENDED_LOG=y CONFIG_NETDEVSIM=m +CONFIG_VLAN_8021Q=m CONFIG_XDP_SOCKETS=y diff --git a/tools/testing/selftests/drivers/net/lib/py/env.py b/tools/testing/selftests/drivers/net/lib/py/env.py index 41cc248ac848..b80666277e36 100644 --- a/tools/testing/selftests/drivers/net/lib/py/env.py +++ b/tools/testing/selftests/drivers/net/lib/py/env.py @@ -255,6 +255,15 @@ class NetDrvEpEnv(NetDrvEnvBase): if nsim_test is False and self._ns is not None: raise KsftXfailEx("Test does not work on netdevsim") + def get_local_nsim_dev(self): + """Returns the local netdevsim device or None. + Using this method is discouraged, as it makes tests nsim-specific. + Standard interfaces available on all HW should ideally be used. + This method is intended for the few cases where nsim-specific + assertions need to be verified which cannot be verified otherwise. + """ + return self._ns + def _require_cmd(self, comm, key, host=None): cached = self._required_cmd.get(comm, {}) if cached.get(key) is None: diff --git a/tools/testing/selftests/drivers/net/macsec.py b/tools/testing/selftests/drivers/net/macsec.py index 5220c0656c0c..9a83d9542e04 100755 --- a/tools/testing/selftests/drivers/net/macsec.py +++ b/tools/testing/selftests/drivers/net/macsec.py @@ -6,10 +6,14 @@ import os from lib.py import ksft_run, ksft_exit, ksft_eq, ksft_raises +from lib.py import ksft_variants, KsftNamedVariant from lib.py import CmdExitFailure, KsftSkipEx from lib.py import NetDrvEpEnv from lib.py import cmd, ip, defer, ethtool +MACSEC_KEY = "12345678901234567890123456789012" +MACSEC_VLAN_VID = 10 + # Unique prefix per run to avoid collisions in the shared netns. # Keep it short: IFNAMSIZ is 16 (incl. NUL), and VLAN names append ".". MACSEC_PFX = f"ms{os.getpid()}_" @@ -59,6 +63,78 @@ def _require_macsec_offload(cfg): raise KsftSkipEx("macsec-hw-offload not supported") +def _get_mac(ifname, host=None): + """Gets MAC address of an interface.""" + dev = ip(f"link show dev {ifname}", json=True, host=host) + return dev[0]["address"] + + +def _setup_macsec_sa(cfg, name): + """Adds matching TX/RX SAs on both ends.""" + local_mac = _get_mac(name) + remote_mac = _get_mac(name, host=cfg.remote) + + ip(f"macsec add {name} tx sa 0 pn 1 on key 01 {MACSEC_KEY}") + ip(f"macsec add {name} rx port 1 address {remote_mac}") + ip(f"macsec add {name} rx port 1 address {remote_mac} " + f"sa 0 pn 1 on key 02 {MACSEC_KEY}") + + ip(f"macsec add {name} tx sa 0 pn 1 on key 02 {MACSEC_KEY}", + host=cfg.remote) + ip(f"macsec add {name} rx port 1 address {local_mac}", host=cfg.remote) + ip(f"macsec add {name} rx port 1 address {local_mac} " + f"sa 0 pn 1 on key 01 {MACSEC_KEY}", host=cfg.remote) + + +def _setup_macsec_devs(cfg, name, offload): + """Creates macsec devices on both ends. + + Only the local device gets HW offload; the remote always uses software + MACsec since it may not support offload at all. + """ + offload_arg = "mac" if offload else "off" + + ip(f"link add link {cfg.ifname} {name} " + f"type macsec encrypt on offload {offload_arg}") + defer(ip, f"link del {name}") + ip(f"link add link {cfg.remote_ifname} {name} " + f"type macsec encrypt on", host=cfg.remote) + defer(ip, f"link del {name}", host=cfg.remote) + + +def _set_offload(name, offload): + """Sets offload on the local macsec device only.""" + offload_arg = "mac" if offload else "off" + + ip(f"link set {name} type macsec encrypt on offload {offload_arg}") + + +def _setup_vlans(cfg, name, vid): + """Adds VLANs on top of existing macsec devs.""" + vlan_name = f"{name}.{vid}" + + ip(f"link add link {name} {vlan_name} type vlan id {vid}") + defer(ip, f"link del {vlan_name}") + ip(f"link add link {name} {vlan_name} type vlan id {vid}", host=cfg.remote) + defer(ip, f"link del {vlan_name}", host=cfg.remote) + + +def _setup_vlan_ips(cfg, name, vid): + """Adds VLANs and IPs and brings up the macsec + VLAN devices.""" + local_ip = "198.51.100.1" + remote_ip = "198.51.100.2" + vlan_name = f"{name}.{vid}" + + ip(f"addr add {local_ip}/24 dev {vlan_name}") + ip(f"addr add {remote_ip}/24 dev {vlan_name}", host=cfg.remote) + ip(f"link set {name} up") + ip(f"link set {name} up", host=cfg.remote) + ip(f"link set {vlan_name} up") + ip(f"link set {vlan_name} up", host=cfg.remote) + + return vlan_name, remote_ip + + def test_offload_api(cfg) -> None: """MACsec offload API: create SecY, add SA/rx, toggle offload.""" @@ -187,6 +263,69 @@ def test_offload_state(cfg) -> None: "features should match first offload-on snapshot") +def _check_nsim_vid(cfg, vid, expected) -> None: + """Checks if a VLAN is present. Only works on netdevsim.""" + + nsim = cfg.get_local_nsim_dev() + if not nsim: + return + + vlan_path = os.path.join(nsim.nsims[0].dfs_dir, "vlan") + with open(vlan_path, encoding="utf-8") as f: + vids = f.read() + found = f"ctag {vid}\n" in vids + ksft_eq(found, expected, + f"VLAN {vid} {'expected' if expected else 'not expected'}" + f" in debugfs") + + +@ksft_variants([ + KsftNamedVariant("offloaded", True), + KsftNamedVariant("software", False), +]) +def test_vlan(cfg, offload) -> None: + """Ping through VLAN-over-macsec.""" + + _require_ip_macsec(cfg) + if offload: + _require_macsec_offload(cfg) + else: + _require_ip_macsec_offload() + name = _macsec_name() + _setup_macsec_devs(cfg, name, offload=offload) + _setup_macsec_sa(cfg, name) + _setup_vlans(cfg, name, MACSEC_VLAN_VID) + vlan_name, remote_ip = _setup_vlan_ips(cfg, name, MACSEC_VLAN_VID) + _check_nsim_vid(cfg, MACSEC_VLAN_VID, offload) + # nsim doesn't handle the data path for offloaded macsec, so skip + # the ping when offloaded on nsim. + if not offload or not cfg.get_local_nsim_dev(): + cmd(f"ping -I {vlan_name} -c 1 -W 5 {remote_ip}") + + +@ksft_variants([ + KsftNamedVariant("on_to_off", True), + KsftNamedVariant("off_to_on", False), +]) +def test_vlan_toggle(cfg, offload) -> None: + """Toggle offload: VLAN filters propagate/remove correctly.""" + + _require_ip_macsec(cfg) + _require_macsec_offload(cfg) + name = _macsec_name() + _setup_macsec_devs(cfg, name, offload=offload) + _setup_vlans(cfg, name, MACSEC_VLAN_VID) + _check_nsim_vid(cfg, MACSEC_VLAN_VID, offload) + _set_offload(name, offload=not offload) + _check_nsim_vid(cfg, MACSEC_VLAN_VID, not offload) + vlan_name, remote_ip = _setup_vlan_ips(cfg, name, MACSEC_VLAN_VID) + _setup_macsec_sa(cfg, name) + # nsim doesn't handle the data path for offloaded macsec, so skip + # the ping when the final state is offloaded on nsim. + if offload or not cfg.get_local_nsim_dev(): + cmd(f"ping -I {vlan_name} -c 1 -W 5 {remote_ip}") + + def main() -> None: """Main program.""" with NetDrvEpEnv(__file__) as cfg: @@ -194,6 +333,8 @@ def main() -> None: test_max_secy, test_max_sc, test_offload_state, + test_vlan, + test_vlan_toggle, ], args=(cfg,)) ksft_exit() -- cgit v1.2.3 From 04013c3ca022734ec2897b28a96d4cbd8a930407 Mon Sep 17 00:00:00 2001 From: Jiayuan Chen Date: Tue, 7 Apr 2026 10:26:28 +0800 Subject: selftests/bpf: Add tests for sock_ops ctx access with same src/dst register Add selftests to verify SOCK_OPS_GET_SK() and SOCK_OPS_GET_FIELD() correctly return NULL/zero when dst_reg == src_reg and is_fullsock == 0. Three subtests are included: - get_sk: ctx->sk with same src/dst register (SOCK_OPS_GET_SK) - get_field: ctx->snd_cwnd with same src/dst register (SOCK_OPS_GET_FIELD) - get_sk_diff_reg: ctx->sk with different src/dst register (baseline) Each BPF program uses inline asm (__naked) to force specific register allocation, reads is_fullsock first, then loads the field using the same (or different) register. The test triggers TCP_NEW_SYN_RECV via a TCP handshake and checks that the result is NULL/zero when is_fullsock == 0. Reviewed-by: Sun Jian Signed-off-by: Jiayuan Chen Acked-by: Martin KaFai Lau Link: https://patch.msgid.link/20260407022720.162151-3-jiayuan.chen@linux.dev Signed-off-by: Jakub Kicinski --- .../selftests/bpf/prog_tests/sock_ops_get_sk.c | 76 +++++++++++++ .../testing/selftests/bpf/progs/sock_ops_get_sk.c | 117 +++++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 tools/testing/selftests/bpf/prog_tests/sock_ops_get_sk.c create mode 100644 tools/testing/selftests/bpf/progs/sock_ops_get_sk.c (limited to 'tools') diff --git a/tools/testing/selftests/bpf/prog_tests/sock_ops_get_sk.c b/tools/testing/selftests/bpf/prog_tests/sock_ops_get_sk.c new file mode 100644 index 000000000000..343d92c4df30 --- /dev/null +++ b/tools/testing/selftests/bpf/prog_tests/sock_ops_get_sk.c @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-2.0 + +#include +#include "cgroup_helpers.h" +#include "network_helpers.h" +#include "sock_ops_get_sk.skel.h" + +/* See progs/sock_ops_get_sk.c for the bug description. */ +static void run_sock_ops_test(int cgroup_fd, int prog_fd) +{ + int server_fd, client_fd, err; + + err = bpf_prog_attach(prog_fd, cgroup_fd, BPF_CGROUP_SOCK_OPS, 0); + if (!ASSERT_OK(err, "prog_attach")) + return; + + server_fd = start_server(AF_INET, SOCK_STREAM, NULL, 0, 0); + if (!ASSERT_OK_FD(server_fd, "start_server")) + goto detach; + + /* Trigger TCP handshake which causes TCP_NEW_SYN_RECV state where + * is_fullsock == 0 and is_locked_tcp_sock == 0. + */ + client_fd = connect_to_fd(server_fd, 0); + if (!ASSERT_OK_FD(client_fd, "connect_to_fd")) + goto close_server; + + close(client_fd); + +close_server: + close(server_fd); +detach: + bpf_prog_detach(cgroup_fd, BPF_CGROUP_SOCK_OPS); +} + +void test_ns_sock_ops_get_sk(void) +{ + struct sock_ops_get_sk *skel; + int cgroup_fd; + + cgroup_fd = test__join_cgroup("/sock_ops_get_sk"); + if (!ASSERT_OK_FD(cgroup_fd, "join_cgroup")) + return; + + skel = sock_ops_get_sk__open_and_load(); + if (!ASSERT_OK_PTR(skel, "skel_open_load")) + goto close_cgroup; + + /* Test SOCK_OPS_GET_SK with same src/dst register */ + if (test__start_subtest("get_sk")) { + run_sock_ops_test(cgroup_fd, + bpf_program__fd(skel->progs.sock_ops_get_sk_same_reg)); + ASSERT_EQ(skel->bss->null_seen, 1, "null_seen"); + ASSERT_EQ(skel->bss->bug_detected, 0, "bug_not_detected"); + } + + /* Test SOCK_OPS_GET_FIELD with same src/dst register */ + if (test__start_subtest("get_field")) { + run_sock_ops_test(cgroup_fd, + bpf_program__fd(skel->progs.sock_ops_get_field_same_reg)); + ASSERT_EQ(skel->bss->field_null_seen, 1, "field_null_seen"); + ASSERT_EQ(skel->bss->field_bug_detected, 0, "field_bug_not_detected"); + } + + /* Test SOCK_OPS_GET_SK with different src/dst register */ + if (test__start_subtest("get_sk_diff_reg")) { + run_sock_ops_test(cgroup_fd, + bpf_program__fd(skel->progs.sock_ops_get_sk_diff_reg)); + ASSERT_EQ(skel->bss->diff_reg_null_seen, 1, "diff_reg_null_seen"); + ASSERT_EQ(skel->bss->diff_reg_bug_detected, 0, "diff_reg_bug_not_detected"); + } + + sock_ops_get_sk__destroy(skel); +close_cgroup: + close(cgroup_fd); +} diff --git a/tools/testing/selftests/bpf/progs/sock_ops_get_sk.c b/tools/testing/selftests/bpf/progs/sock_ops_get_sk.c new file mode 100644 index 000000000000..3a0689f8ce7c --- /dev/null +++ b/tools/testing/selftests/bpf/progs/sock_ops_get_sk.c @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: GPL-2.0 + +#include "vmlinux.h" +#include +#include "bpf_misc.h" + +/* + * Test the SOCK_OPS_GET_SK() and SOCK_OPS_GET_FIELD() macros in + * sock_ops_convert_ctx_access() when dst_reg == src_reg. + * + * When dst_reg == src_reg, the macros borrow a temporary register to load + * is_fullsock / is_locked_tcp_sock, because dst_reg holds the ctx pointer + * and cannot be clobbered before ctx->sk / ctx->field is read. If + * is_fullsock == 0 (e.g., TCP_NEW_SYN_RECV with a request_sock), the macro + * must still zero dst_reg so the verifier's PTR_TO_SOCKET_OR_NULL / + * SCALAR_VALUE type is correct at runtime. A missing clear leaves a stale + * ctx pointer in dst_reg that passes NULL checks (GET_SK) or leaks a kernel + * address as a scalar (GET_FIELD). + * + * When dst_reg != src_reg, dst_reg itself is used to load is_fullsock, so + * the JEQ (dst_reg == 0) naturally leaves it zeroed on the !fullsock path. + */ + +int bug_detected; +int null_seen; + +SEC("sockops") +__naked void sock_ops_get_sk_same_reg(void) +{ + asm volatile ( + "r7 = *(u32 *)(r1 + %[is_fullsock_off]);" + "r1 = *(u64 *)(r1 + %[sk_off]);" + "if r7 != 0 goto 2f;" + "if r1 == 0 goto 1f;" + "r1 = %[bug_detected] ll;" + "r2 = 1;" + "*(u32 *)(r1 + 0) = r2;" + "goto 2f;" + "1:" + "r1 = %[null_seen] ll;" + "r2 = 1;" + "*(u32 *)(r1 + 0) = r2;" + "2:" + "r0 = 1;" + "exit;" + : + : __imm_const(is_fullsock_off, offsetof(struct bpf_sock_ops, is_fullsock)), + __imm_const(sk_off, offsetof(struct bpf_sock_ops, sk)), + __imm_addr(bug_detected), + __imm_addr(null_seen) + : __clobber_all); +} + +/* SOCK_OPS_GET_FIELD: same-register, is_locked_tcp_sock == 0 path. */ +int field_bug_detected; +int field_null_seen; + +SEC("sockops") +__naked void sock_ops_get_field_same_reg(void) +{ + asm volatile ( + "r7 = *(u32 *)(r1 + %[is_fullsock_off]);" + "r1 = *(u32 *)(r1 + %[snd_cwnd_off]);" + "if r7 != 0 goto 2f;" + "if r1 == 0 goto 1f;" + "r1 = %[field_bug_detected] ll;" + "r2 = 1;" + "*(u32 *)(r1 + 0) = r2;" + "goto 2f;" + "1:" + "r1 = %[field_null_seen] ll;" + "r2 = 1;" + "*(u32 *)(r1 + 0) = r2;" + "2:" + "r0 = 1;" + "exit;" + : + : __imm_const(is_fullsock_off, offsetof(struct bpf_sock_ops, is_fullsock)), + __imm_const(snd_cwnd_off, offsetof(struct bpf_sock_ops, snd_cwnd)), + __imm_addr(field_bug_detected), + __imm_addr(field_null_seen) + : __clobber_all); +} + +/* SOCK_OPS_GET_SK: different-register, is_fullsock == 0 path. */ +int diff_reg_bug_detected; +int diff_reg_null_seen; + +SEC("sockops") +__naked void sock_ops_get_sk_diff_reg(void) +{ + asm volatile ( + "r7 = r1;" + "r6 = *(u32 *)(r7 + %[is_fullsock_off]);" + "r2 = *(u64 *)(r7 + %[sk_off]);" + "if r6 != 0 goto 2f;" + "if r2 == 0 goto 1f;" + "r1 = %[diff_reg_bug_detected] ll;" + "r3 = 1;" + "*(u32 *)(r1 + 0) = r3;" + "goto 2f;" + "1:" + "r1 = %[diff_reg_null_seen] ll;" + "r3 = 1;" + "*(u32 *)(r1 + 0) = r3;" + "2:" + "r0 = 1;" + "exit;" + : + : __imm_const(is_fullsock_off, offsetof(struct bpf_sock_ops, is_fullsock)), + __imm_const(sk_off, offsetof(struct bpf_sock_ops, sk)), + __imm_addr(diff_reg_bug_detected), + __imm_addr(diff_reg_null_seen) + : __clobber_all); +} + +char _license[] SEC("license") = "GPL"; -- cgit v1.2.3 From 8dd1bdde38af8418889ba322a3663c401a60fe28 Mon Sep 17 00:00:00 2001 From: Jiayuan Chen Date: Sat, 11 Apr 2026 08:55:20 +0800 Subject: selftests/bpf: add test for xdp_master_redirect with bond not up Add a selftest that reproduces the null-ptr-deref in bond_rr_gen_slave_id() when XDP redirect targets a bond device in round-robin mode that was never brought up. The test verifies the fix by ensuring no crash occurs. Test setup: - bond0: active-backup mode, UP, with native XDP (enables bpf_master_redirect_enabled_key globally) - bond1: round-robin mode, never UP - veth1: slave of bond1, with generic XDP (XDP_TX) - BPF_PROG_TEST_RUN with live frames triggers the redirect path Signed-off-by: Jiayuan Chen Link: https://patch.msgid.link/20260411005524.201200-3-jiayuan.chen@linux.dev Signed-off-by: Paolo Abeni --- .../testing/selftests/bpf/prog_tests/xdp_bonding.c | 96 +++++++++++++++++++++- 1 file changed, 94 insertions(+), 2 deletions(-) (limited to 'tools') diff --git a/tools/testing/selftests/bpf/prog_tests/xdp_bonding.c b/tools/testing/selftests/bpf/prog_tests/xdp_bonding.c index e8ea26464349..c42488e445c2 100644 --- a/tools/testing/selftests/bpf/prog_tests/xdp_bonding.c +++ b/tools/testing/selftests/bpf/prog_tests/xdp_bonding.c @@ -191,13 +191,18 @@ fail: return -1; } -static void bonding_cleanup(struct skeletons *skeletons) +static void link_cleanup(struct skeletons *skeletons) { - restore_root_netns(); while (skeletons->nlinks) { skeletons->nlinks--; bpf_link__destroy(skeletons->links[skeletons->nlinks]); } +} + +static void bonding_cleanup(struct skeletons *skeletons) +{ + restore_root_netns(); + link_cleanup(skeletons); ASSERT_OK(system("ip link delete bond1"), "delete bond1"); ASSERT_OK(system("ip link delete veth1_1"), "delete veth1_1"); ASSERT_OK(system("ip link delete veth1_2"), "delete veth1_2"); @@ -493,6 +498,90 @@ out: system("ip link del bond_nest2"); } +/* + * Test that XDP redirect via xdp_master_redirect() does not crash when + * the bond master device is not up. When bond is in round-robin mode but + * never opened, rr_tx_counter is NULL. + */ +static void test_xdp_bonding_redirect_no_up(struct skeletons *skeletons) +{ + struct nstoken *nstoken = NULL; + int xdp_pass_fd; + int veth1_ifindex; + int err; + char pkt[ETH_HLEN + 1]; + struct xdp_md ctx_in = {}; + + DECLARE_LIBBPF_OPTS(bpf_test_run_opts, opts, + .data_in = &pkt, + .data_size_in = sizeof(pkt), + .ctx_in = &ctx_in, + .ctx_size_in = sizeof(ctx_in), + .flags = BPF_F_TEST_XDP_LIVE_FRAMES, + .repeat = 1, + .batch_size = 1, + ); + + /* We can't use bonding_setup() because bond will be active */ + SYS(out, "ip netns add ns_rr_no_up"); + nstoken = open_netns("ns_rr_no_up"); + if (!ASSERT_OK_PTR(nstoken, "open ns_rr_no_up")) + goto out; + + /* bond0: active-backup, UP with slave veth0. + * Attaching native XDP to bond0 enables bpf_master_redirect_enabled_key + * globally. + */ + SYS(out, "ip link add bond0 type bond mode active-backup"); + SYS(out, "ip link add veth0 type veth peer name veth0p"); + SYS(out, "ip link set veth0 master bond0"); + SYS(out, "ip link set bond0 up"); + SYS(out, "ip link set veth0p up"); + + /* bond1: round-robin, never UP -> rr_tx_counter stays NULL */ + SYS(out, "ip link add bond1 type bond mode balance-rr"); + SYS(out, "ip link add veth1 type veth peer name veth1p"); + SYS(out, "ip link set veth1 master bond1"); + + veth1_ifindex = if_nametoindex("veth1"); + if (!ASSERT_GT(veth1_ifindex, 0, "veth1_ifindex")) + goto out; + + /* Attach native XDP to bond0 -> enables global redirect key */ + if (xdp_attach(skeletons, skeletons->xdp_tx->progs.xdp_tx, "bond0")) + goto out; + + /* Attach generic XDP (XDP_TX) to veth1. + * When packets arrive at veth1 via netif_receive_skb, do_xdp_generic() + * runs this program. XDP_TX + bond slave triggers xdp_master_redirect(). + */ + err = bpf_xdp_attach(veth1_ifindex, + bpf_program__fd(skeletons->xdp_tx->progs.xdp_tx), + XDP_FLAGS_SKB_MODE, NULL); + if (!ASSERT_OK(err, "attach generic XDP to veth1")) + goto out; + + /* Run BPF_PROG_TEST_RUN with XDP_PASS live frames on veth1. + * XDP_PASS frames become SKBs with skb->dev = veth1, entering + * netif_receive_skb -> do_xdp_generic -> xdp_master_redirect. + * Without the fix, bond_rr_gen_slave_id() dereferences NULL + * rr_tx_counter and crashes. + */ + xdp_pass_fd = bpf_program__fd(skeletons->xdp_dummy->progs.xdp_dummy_prog); + + memset(pkt, 0, sizeof(pkt)); + ctx_in.data_end = sizeof(pkt); + ctx_in.ingress_ifindex = veth1_ifindex; + + err = bpf_prog_test_run_opts(xdp_pass_fd, &opts); + ASSERT_OK(err, "xdp_pass test_run should not crash"); + +out: + link_cleanup(skeletons); + close_netns(nstoken); + SYS_NOFAIL("ip netns del ns_rr_no_up"); +} + static void test_xdp_bonding_features(struct skeletons *skeletons) { LIBBPF_OPTS(bpf_xdp_query_opts, query_opts); @@ -738,6 +827,9 @@ void serial_test_xdp_bonding(void) if (test__start_subtest("xdp_bonding_redirect_multi")) test_xdp_bonding_redirect_multi(&skeletons); + if (test__start_subtest("xdp_bonding_redirect_no_up")) + test_xdp_bonding_redirect_no_up(&skeletons); + out: xdp_dummy__destroy(skeletons.xdp_dummy); xdp_tx__destroy(skeletons.xdp_tx); -- cgit v1.2.3