From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from smtpout-02.galae.net (smtpout-02.galae.net [185.246.84.56]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 76ACC4FD799; Thu, 15 Oct 2026 13:44:15 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=185.246.84.56 ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1792071858; cv=none; b=OxJS2X4VhiiupRZOymJS3jx1xKkFaAsVZ/0Z6C//NZMku2Q2prPzL4Dfl7/cDIWjrb9XsSPwYhk9pH7iEjMKBUXcb7uR9W1PxrwO+Q2oBSOtUeg/E6+B7uT1Nql1RxT+ekXAmGomskcZkR3namyOwcj1EbgNIbkI5Byb1xaiUKg= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1792071858; c=relaxed/simple; bh=llaMvQoFSdnQO0mSZ9aYBtvHQMDrFhlJ6fZzQyCa410=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=P9py3wdgnFJNpOD79xAz6oM8F4GnH/PZ+0KJz/emDyNYWl52/c/WOD+l6fC/SOxVeBAHBzxCVfQWuAuXOt5jErMZ3W+6gzKE4jicf7FP5ysDe7CqcnDYlRBU/RkMUlQrRFdE6bamDGUurfTCLoqAf3sMn2GgnnMY5saG3umY7UM= ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=bootlin.com; spf=pass smtp.mailfrom=bootlin.com; dkim=pass (2048-bit key) header.d=bootlin.com header.i=@bootlin.com header.b=nLaPxNBU; arc=none smtp.client-ip=185.246.84.56 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=bootlin.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=bootlin.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=bootlin.com header.i=@bootlin.com header.b="nLaPxNBU" Received: from smtpout-01.galae.net (smtpout-01.galae.net [212.83.139.233]) by smtpout-02.galae.net (Postfix) with ESMTPS id BC6771A123F; Thu, 15 Oct 2026 13:44:13 +0000 (UTC) Received: from mail.galae.net (mail.galae.net [212.83.136.155]) by smtpout-01.galae.net (Postfix) with ESMTPS id 88F0F60360; Thu, 15 Oct 2026 13:44:13 +0000 (UTC) Received: from [127.0.0.1] (localhost [127.0.0.1]) by localhost (Mailerdaemon) with ESMTPSA id 8A1CF10328C54; Thu, 15 Oct 2026 15:44:05 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=bootlin.com; s=dkim; t=1792071848; h=from:subject:date:message-id:to:cc:mime-version: content-transfer-encoding:in-reply-to:references; bh=hD0TfzYKHHUCMYIuN7U59DctijTqjZLpc9JhPDnzTuw=; b=nLaPxNBU/6IT/at3PVrAFnPPI/kF8gsUnYdWaV/Y4ogNFkwNcXcoBssX/LYP9VgnLpLTW/ sV9FKNrHJraoNjEg/fmE4bV64Lk3YtNp5Cl8GmNG+p/AAqqOrs+b1zC/HPfIDSemrWVwWI H3o/PPpyGXMOByLHfqMFNHxS1+6Mvl8ko0tBD46Y98lTJ8s8V2brNm4BV+DQe426IwFve1 p5ofcDwvMBfoGcYicpzAbqdIdr4U+bM4TFfHwujeT3jko/oqheb0KiOmgBaeGSKXPUNXaT AR6zhb5j4as3SvXQJXJ/UdszWSuflx2Rcu7JKjIbodrp+G6vQD7TvQQxTBO92Q== From: "Maxime Chevallier (Netdev Foundation)" To: Andrew Lunn , Jakub Kicinski , davem@davemloft.net, Eric Dumazet , Paolo Abeni , Simon Horman , Russell King , Heiner Kallweit , Jonathan Corbet , Shuah Khan Cc: "Maxime Chevallier (Netdev Foundation)" , Oleksij Rempel , Vladimir Oltean , Florian Fainelli , thomas.petazzoni@bootlin.com, netdev@vger.kernel.org, linux-kernel@vger.kernel.org, linux-doc@vger.kernel.org Subject: [PATCH net-next v3 1/3] selftests: drv-net: Introduce a selftest for ethtool flow control Date: Thu, 15 Oct 2026 15:43:46 +0200 Message-ID: <20261015134350.629593-2-maxime.chevallier@bootlin.com> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20261015134350.629593-1-maxime.chevallier@bootlin.com> References: <20261015134350.629593-1-maxime.chevallier@bootlin.com> Precedence: bulk X-Mailing-List: linux-doc@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Last-TLS-Session-Version: TLSv1.3 Ethernet flow control is a tricky thing to get right, especially when it comes to correctly handling the negotiation of the parameters with a link partner. On one hand, the userspace API talks in terms of the device's ability to send Pause frames when the device is overwhelmed by ingress traffic (TX pause) and the ability to stop sending traffic upon receiving Pause frames (RX pause). On the other hand, 802.3 explains that devices can exchange their abilities over link negotiation, but instead of echanging TX and RX abilities, they exchange Pause and Asymmetric Pause capabilities : RX unable TX unable => None RX able TX unable => Pause + Asymmetric RX unable TX able => Asymmetric RX able TX able => Pause The reported Pause and Asym abilities depend both on the MAC, that eventually sends and processes these frames, and the PHY, that advertises these to the partner. Both MAC and PHYs can have their own limitations, and getting the correct set of supported Pause/Asym parameters is non-trivial, unless the MAC uses phylink, which deals with the complexity. Introduce a set of Pause selftests that verify that the local interface's supported pause parameters reported from ethtool (in terms of Pause + Asym ) match the accepted parameters from "ethtool -A", corresponding to ethtool's .set_pauseparams() ops, expressed in TX and RX abilities. Signed-off-by: Maxime Chevallier (Netdev Foundation) --- .../testing/selftests/drivers/net/hw/Makefile | 1 + .../testing/selftests/drivers/net/hw/pause.py | 369 ++++++++++++++++++ 2 files changed, 370 insertions(+) create mode 100755 tools/testing/selftests/drivers/net/hw/pause.py diff --git a/tools/testing/selftests/drivers/net/hw/Makefile b/tools/testing/selftests/drivers/net/hw/Makefile index 18ce235a77f8..9b6b5d15f04d 100644 --- a/tools/testing/selftests/drivers/net/hw/Makefile +++ b/tools/testing/selftests/drivers/net/hw/Makefile @@ -40,6 +40,7 @@ TEST_PROGS = \ nk_netns.py \ nk_qlease.py \ ntuple.py \ + pause.py \ pp_alloc_fail.py \ rss_api.py \ rss_ctx.py \ diff --git a/tools/testing/selftests/drivers/net/hw/pause.py b/tools/testing/selftests/drivers/net/hw/pause.py new file mode 100755 index 000000000000..854428e19f55 --- /dev/null +++ b/tools/testing/selftests/drivers/net/hw/pause.py @@ -0,0 +1,369 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 + +""" +Driver-related behavior tests for Pause-based Flow Control. +""" + +import errno +import json +import os + +from lib.py import KsftFailEx, KsftNamedVariant, KsftSkipEx +from lib.py import NetDrvEpEnv +from lib.py import cmd, defer, ethtool, ip, wait_file +from lib.py import ksft_disruptive, ksft_variants, ksft_run +from lib.py import ksft_eq, ksft_exit, ksft_in, ksft_not_in, ksft_pr + +# Linkmodes to Pause params : +# Pause bit is set if rx == 1 +# Asym_Pause bit is set if rx != tx +pauseparams_to_linkmodes = { + 0: {0: {"rx": 0, "tx": 0, "linkmodes": []}, + 1: {"rx": 0, "tx": 1, "linkmodes": ["Asym_Pause"]}}, + 1: {0: {"rx": 1, "tx": 0, "linkmodes": ["Pause", "Asym_Pause"]}, + 1: {"rx": 1, "tx": 1, "linkmodes": ["Pause"]}}, +} + +def onoff(val): + """ Convert a bool to on/off """ + return "on" if val else "off" + +pauseparams_variants = [ + KsftNamedVariant(f"RX {onoff(p['rx'])} TX {onoff(p['tx'])}", p) + for by_tx in pauseparams_to_linkmodes.values() for p in by_tx.values() +] + +_strerrors = {os.strerror(e): e for e in errno.errorcode} + +def ethtool_ret(command, is_get=True, host=None): + """ Execute an ethtool command, returns the return code and JSON content + + :param command: the ethtool arguments + :param is_get: Is the command a get or a set. Get commands return the loaded + JSON attributes + :param host: The host on which to run the command on. None means local host. + """ + json_flag = "--json" if is_get else "" + cmd_res = cmd(f"ethtool {json_flag} {command}", host=host, fail=False) + + if cmd_res.ret != 0: + # ethtool returns 1 upon error, not the netlink errcode. Try to get it + # by parsing the stderr output, which looks like : + # "netlink error: Operation not supported" + for line in cmd_res.stderr.splitlines(): + err = _strerrors.get(line.rsplit(": ", 1)[-1].strip()) + if err: + return err, None + return cmd_res.ret, None + + # Not a get operation, we don't have any JSON output to parse + if not is_get: + return 0, None + + return 0, json.loads(cmd_res.stdout)[0] + +def wait_for_link(cfg, timeout=15): + """ Wait for the link to come up """ + + wait_file(f"/sys/class/net/{cfg.ifname}/carrier", + lambda x: x.strip() == "1", deadline=timeout) + +def wait_for_aneg(cfg, link_drop=False, timeout=15): + """ Wait for a renegotiation to complete + + :param link_drop: Set to true if the link HAS to flap. + """ + try: + wait_file(f"/sys/class/net/{cfg.ifname}/carrier", + lambda x: x.strip() == "0", + deadline=timeout if link_drop else 2) + except TimeoutError: + if link_drop: + raise + + wait_for_link(cfg, timeout) + +def forced_link_settings(cfg): + """ Returns a string to pass to ethtool -s with speed/duplex corresponding + to the current settings. + + Note that some devices don't return duplex info, so assume full duplex + in that case. + """ + link = ethtool(f"{cfg.ifname}", json=True)[0] + if "speed" not in link: + return "" + + return f"speed {link['speed']} duplex {link.get('duplex', 'Full').lower()}" + +def _ethtool_pause_use_to_linkmodes(use): + """ Convert the ethtool output for pause modes into pause linkmodes """ + if use == "Symmetric": + return ["Pause"] + elif use == "Symmetric Receive-only": + return ["Pause", "Asym_Pause"] + elif use == "Transmit-only": + return ["Asym_Pause"] + else: + return [] + +def set_local_pauseparams(cfg, rx, tx, aneg): + """ set pauseparams : ethtool -A """ + rx_param = onoff(rx) + tx_param = onoff(tx) + aneg_param = onoff(aneg) + + ret, _ = ethtool_ret(f"-A {cfg.ifname} rx {rx_param} tx {tx_param}" + f" autoneg {aneg_param}", + is_get = False) + + if ret != 0 and ret != errno.EOPNOTSUPP: + raise KsftFailEx(f"ethtool -A {cfg.ifname} failed: {ret}") + + return ret + +def get_local_pauseparams(cfg): + """ get pauseparams : ethtool -a """ + return ethtool_ret(f"-a {cfg.ifname}", is_get=True) + +def get_local_pause_supported(cfg): + """ Return the errcode and the local pause supported linkmodes """ + + ret, data = ethtool_ret(f"{cfg.ifname}") + if ret != 0: + raise KsftFailEx(f"ethtool {cfg.ifname} failed: {ret}") + + if "supported-pause-frame-use" not in data: + raise KsftSkipEx(f"{cfg.ifname} doesn't report its link modes") + + return ret, _ethtool_pause_use_to_linkmodes(data["supported-pause-frame-use"]) + +def get_local_pause_advertising(cfg): + """ Return the errcode and the local pause advertised linkmodes """ + + ret, data = ethtool_ret(f"{cfg.ifname}") + if ret != 0: + raise KsftFailEx(f"ethtool {cfg.ifname} failed: {ret}") + + if "advertised-pause-frame-use" not in data: + raise KsftSkipEx(f"{cfg.ifname} doesn't report its link modes") + + return ret, _ethtool_pause_use_to_linkmodes(data["advertised-pause-frame-use"]) + +def require_pause_supported_allof(cfg, linkmodes): + """ Skip if local device doesn't support all of the passed modes """ + + ret, _ = get_local_pauseparams(cfg) + if ret != 0: + raise KsftSkipEx("device doesn't allow getting pauseparams") + + _, pause_support = get_local_pause_supported(cfg) + for lm in linkmodes: + if lm not in pause_support: + raise KsftSkipEx(f"Local device doesn't support {lm}") + +def expect_pauseparams_set(cfg, pauseparams, aneg, supported, note): + """ ethtool -A must work or be refused depending on what the local + device supports + """ + rx = pauseparams["rx"] + tx = pauseparams["tx"] + + # Measure current pauseparams, re-setting the same params as the current + # ones returns 0 even if unsupported. + _, cur = get_local_pauseparams(cfg) + noop = cur and (cur["rx"], cur["tx"], cur["autonegotiate"]) == \ + (bool(rx), bool(tx), aneg) + + ret, _ = ethtool_ret(f"-A {cfg.ifname} rx {onoff(rx)} tx {onoff(tx)}" + f" autoneg {onoff(aneg)}", is_get = False) + + if noop: + ksft_eq(ret, 0, note) + elif not supported: + ksft_eq(ret, errno.EOPNOTSUPP, note) + elif set(pauseparams["linkmodes"]).issubset(set(supported)): + ksft_eq(ret, 0, note) + else: + ksft_in(ret, (errno.EOPNOTSUPP, errno.EINVAL), note) + +def pause_setup(cfg): + """ The starting conditions every test counts on, restored on exit: + - both ports admin up + - link autoneg on on both sides ifsupported + - carrier on + """ + + # Get init pause parameters + ret, params = ethtool_ret(f"-a {cfg.ifname}") + if ret == 0: + defer(cmd, f"ethtool -A {cfg.ifname} rx {onoff(params['rx'])} " + f"tx {onoff(params['tx'])} " + f"autoneg {onoff(params['autonegotiate'])}") + + # Get init link parameters + link = ethtool(f"{cfg.ifname}", json=True)[0] + if "auto-negotiation" not in link: + raise KsftSkipEx(f"{cfg.ifname} doesn't report its link modes") + + if link["auto-negotiation"]: + defer(cmd, f"ethtool -s {cfg.ifname} autoneg on") + elif "speed" in link and "duplex" in link: + defer(cmd, f"ethtool -s {cfg.ifname} autoneg off speed {link['speed']} " + f"duplex {link['duplex'].lower()}") + + # Local interface admin up, link aneg on + ip(f"link set {cfg.ifname} up") + if link["supports-auto-negotiation"] and not link["auto-negotiation"]: + ethtool(f"-s {cfg.ifname} autoneg on") + + # Get remote pause params + ret, params = ethtool_ret(f"-a {cfg.remote_ifname}", host=cfg.remote) + if ret == 0: + defer(cmd, f"ethtool -A {cfg.remote_ifname} rx {onoff(params['rx'])} " + f"tx {onoff(params['tx'])} " + f"autoneg {onoff(params['autonegotiate'])}", + host=cfg.remote) + + # Get remote link params + link = ethtool(f"{cfg.remote_ifname}", json=True, host=cfg.remote)[0] + if "auto-negotiation" not in link: + raise KsftSkipEx(f"{cfg.remote_ifname} doesn't report its link modes") + + if link["auto-negotiation"]: + defer(cmd, f"ethtool -s {cfg.remote_ifname} autoneg on", + host=cfg.remote) + elif "speed" in link and "duplex" in link: + defer(cmd, f"ethtool -s {cfg.remote_ifname} autoneg off " + f"speed {link['speed']} duplex {link['duplex'].lower()}", + host=cfg.remote) + + # Remote interface admin up, link aneg on + ip(f"link set {cfg.remote_ifname} up", host=cfg.remote) + if link["supports-auto-negotiation"] and not link["auto-negotiation"]: + ethtool(f"-s {cfg.remote_ifname} autoneg on", host=cfg.remote) + + wait_for_link(cfg) + +# Pause support : Supported linkmodes vs ability to set/get pauseparams +@ksft_variants(pauseparams_variants) +@ksft_disruptive +def pause_test_support(cfg, pauseparams): + """ Verify that the supported linkmodes Pause and Asym_Pause match the + ability to configure the rx and tx pauseparams. + + Drivers are expected to reject pauseparams they don't support, and + accept the ones they support. The supported modes are exposed by + the MAC to the PHY layer through phylink mac_capabilities MAC_SYM_PAUSE + and MAC_ASYM_PAUSE, or through phylib directly with the + phy_support_sym_pause() and phy_support_asym_pause() helpers. + + The expectation is for drivers to refuse setting pauseparams that don't + match the Pause and Asym_Pause bits in the supported linkmodes with a + -EOPNOTSUPP return value. Unsupported pause params must be rejected. + + Failing this test likely means the MAC driver doesn't implement the + set/get_pauseparam, but still sets flow control as supported through + phylink mac_capabilities or phylib's pause API. Conversely, the MAC driver + may have omitted to indicate its supported Pause modes. Finally, the PHY + driver may incorrectly override the Pause and Asym_Pause bits in its + supported fields. + """ + + pause_setup(cfg) + + forced = forced_link_settings(cfg) + _, supported = get_local_pause_supported(cfg) + + # We check that what we can configure in the pause params matches what we + # support under various contditions : Link aneg on/off, pause aneg on/off + ret, _ = ethtool_ret(f"-s {cfg.ifname} autoneg on", is_get = False) + if ret != 0: + ksft_pr("link autoneg on refused, not tested") + else: + expect_pauseparams_set(cfg, pauseparams, False, supported, + "link autoneg on") + expect_pauseparams_set(cfg, pauseparams, True, supported, + "link autoneg on") + + if not forced: + ksft_pr("link speed unknown, the forced link is not tested") + return + + ret, _ = ethtool_ret(f"-s {cfg.ifname} autoneg off {forced}", is_get = False) + if ret != 0: + ksft_pr(f"link autoneg off {forced} refused, not tested") + return + + expect_pauseparams_set(cfg, pauseparams, False, supported, "link autoneg off") + expect_pauseparams_set(cfg, pauseparams, True, supported, "link autoneg off") + +@ksft_variants(pauseparams_variants) +@ksft_disruptive +def pause_advertising_test(cfg, pauseparams): + """Pause advertisement + + Validate that changing pause params through the ETHTOOL_MSG_PAUSE command + translates to a change in the advertised pause params, and that these + parameters are correct w.r.t the supported pause params and requested pause + params. + + This exercises the .set_pauseparam() ethtool ops for MAC configuration, + as well as the reconfiguration of the PHY's advertising and negotiation. + + On non-phylink MACs, the MAC should call phy_set_sym_pause() to update the + PHY's advertising, and restart a negotiation with phy_start_aneg() if + need be. Failure to do so will result in the wrong advertising parameters. + + On phylink-enabled MACs, phylink deals with the PHY reconfiguration provided + the MAC driver calls phylink_ethtool_set_pauseparam(). + + Failing this test likely means that the PHY driver is not correctly + advertising pause settings, either due to the MAC not triggering a PHY + reconfiguration, a misconfiguration of the advertising registers by the PHY, + or by mis-handling the phydev->advertising bitmap in the PHY driver directly. + + The validation is made by looking at the advertised modes locally, as well + as what the peer's 'lp_advertising' values report. + """ + + require_pause_supported_allof(cfg, pauseparams["linkmodes"]) + pause_setup(cfg) + + tx = pauseparams["tx"] + rx = pauseparams["rx"] + adv = pauseparams["linkmodes"] + not_adv = [ l for l in ["Pause", "Asym_Pause"] if l not in adv] + + # It's OK to skip here, we're already validating the EOPNOTSUPP behaviour + # the pause_test_support test. + ret = set_local_pauseparams(cfg, rx, tx, True) + if ret == errno.EOPNOTSUPP: + raise KsftSkipEx(f"RX {rx} TX {tx} not supported") + + # Wait for link parameters to re-negotiate and link to come back up. It must + # come back up, otherwise that means changing pauseparams can bring the + # link down. + wait_for_aneg(cfg) + + _, linkmodes = get_local_pause_advertising(cfg) + for mode in adv: + ksft_in(mode, linkmodes, + f"rx {rx} tx {tx} aneg on must advertise {adv}") + + for mode in not_adv: + ksft_not_in(mode, linkmodes, + f"rx {rx} tx {tx} aneg on must not advertise {not_adv}") + +def main(): + """ Entry point """ + with NetDrvEpEnv(__file__, nsim_test=False) as cfg: + ksft_run([pause_test_support, + pause_advertising_test, + ], + args=(cfg, )) + ksft_exit() + +if __name__ == "__main__": + main() -- 2.55.0