mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH net-next 0/3] selftests: drv-net: Introduce flow-control selftest
@ 2026-09-20 16:47 Maxime Chevallier (Netdev Foundation)
  2026-09-20 16:47 ` [PATCH net-next 1/3] selftests: drv-net: Introduce a selftest for ethtool flow control Maxime Chevallier (Netdev Foundation)
                   ` (4 more replies)
  0 siblings, 5 replies; 6+ messages in thread
From: Maxime Chevallier (Netdev Foundation) @ 2026-09-20 16:47 UTC (permalink / raw)
  To: Andrew Lunn, Jakub Kicinski, davem, 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, netdev, linux-kernel, linux-doc

Hi,

This series introduces a test suite for Flow Control, to validate that
drivers behave correctly according to 802.3, but also in a homogenous
way.

Flow control is tricky to get right, for multiple reasons :
 - The negotiation process uses Pause + Asym, while the ethtool API to
   configure pause with TX and RX modes

 - It involves multiple drivers :

  - The MAC driver implements the actual Pause frame handling and
    generation by controlling and reporting these settings to/from the
    hardware. It's also in charge of reporting what the MAC can do.

  - The PHY driver does the actual Pause parameters negociation with the
    LP, and reports that to the MAC

  - Some PCS are also involved in pause to some extent.

phylink helps to some extent, as it implements a lot of the 802.3
translation between RX/TX and Pause/Asym, but not all drivers use
phylink.

Some PHY drivers also tweak the pause advertising, due to HW issues. For
example, KSZ9031 may fail to establish link when advertising Asym Pause.

On top of that, there are 2 toggles for pause autoneg :

 - Link-wide autoneg, dictating whether or not we negotiate ANY
   parameter with the partner
 - Pause autoneg, that only dictates the Pause advertising

To top it all off, the Pause autonegotiation isn't a strict "pick modes
that match exactly what users requested on both ends of the link", the
negotiated pause params may actually differ from what the user asked.

This set of tests is aimed at catching all these issues.

This was tested on a wide variety of HW, some that support Pause + Asym,
some that only support Pause, with a peer that can or can't do pause.

Some issues were found, e.g. even with phylink, setting pause tx off rx
off aneg on makes the local interface still advertise Pause + Asym.

Tested on Macchiatobin, Espressobin, Turris omnia, and around 10
different boards that run stmmac.

Thanks to the Netdev Foundation for funding that work,

Maxime

Maxime Chevallier (Netdev Foundation) (3):
  selftests: drv-net: Introduce a selftest for ethtool flow control
  selftests: drv-net: pause: Validate the pause autonegotiation with a
    partner
  selftests: drv-net: pause: Validate pause autoneg interactions with
    link autoneg

 .../testing/selftests/drivers/net/hw/Makefile |   1 +
 .../drivers/net/hw/lib/py/__init__.py         |   7 +-
 .../testing/selftests/drivers/net/hw/pause.py | 973 ++++++++++++++++++
 .../selftests/drivers/net/lib/py/__init__.py  |   7 +-
 .../selftests/drivers/net/lib/py/ethtool.py   | 205 ++++
 5 files changed, 1191 insertions(+), 2 deletions(-)
 create mode 100755 tools/testing/selftests/drivers/net/hw/pause.py
 create mode 100644 tools/testing/selftests/drivers/net/lib/py/ethtool.py

-- 
2.55.0


^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH net-next 1/3] selftests: drv-net: Introduce a selftest for ethtool flow control
  2026-09-20 16:47 [PATCH net-next 0/3] selftests: drv-net: Introduce flow-control selftest Maxime Chevallier (Netdev Foundation)
@ 2026-09-20 16:47 ` Maxime Chevallier (Netdev Foundation)
  2026-09-20 16:47 ` [PATCH net-next 2/3] selftests: drv-net: pause: tests against a controllable link partner Maxime Chevallier (Netdev Foundation)
                   ` (3 subsequent siblings)
  4 siblings, 0 replies; 6+ messages in thread
From: Maxime Chevallier (Netdev Foundation) @ 2026-09-20 16:47 UTC (permalink / raw)
  To: Andrew Lunn, Jakub Kicinski, davem, 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, netdev, linux-kernel, linux-doc

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.

One 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

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.

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.

All the selftests need to set a link's pauseparams, wait for link up,
wait for autoneg, configure a peer, etc. Future ethtool selftests are
expected to use the same, these helpers are put in the net selftest lib.

Signed-off-by: Maxime Chevallier (Netdev Foundation) <maxime.chevallier@bootlin.com>
---
 .../testing/selftests/drivers/net/hw/Makefile |   1 +
 .../drivers/net/hw/lib/py/__init__.py         |   6 +-
 .../testing/selftests/drivers/net/hw/pause.py | 353 ++++++++++++++++++
 .../selftests/drivers/net/lib/py/__init__.py  |   6 +-
 .../selftests/drivers/net/lib/py/ethtool.py   | 133 +++++++
 5 files changed, 497 insertions(+), 2 deletions(-)
 create mode 100755 tools/testing/selftests/drivers/net/hw/pause.py
 create mode 100644 tools/testing/selftests/drivers/net/lib/py/ethtool.py

diff --git a/tools/testing/selftests/drivers/net/hw/Makefile b/tools/testing/selftests/drivers/net/hw/Makefile
index bd3b8d2fa47e..f0aeb70c20fd 100644
--- a/tools/testing/selftests/drivers/net/hw/Makefile
+++ b/tools/testing/selftests/drivers/net/hw/Makefile
@@ -44,6 +44,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/lib/py/__init__.py b/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py
index 8a58cb17cc06..c79dacf3bfdf 100644
--- a/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py
+++ b/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py
@@ -33,6 +33,8 @@ try:
         ksft_ne, ksft_not_in, ksft_raises, ksft_true, ksft_gt, ksft_not_none
     from drivers.net.lib.py import GenerateTraffic, Remote, Iperf3Runner
     from drivers.net.lib.py import NetDrvEnv, NetDrvEpEnv, NetDrvContEnv
+    from drivers.net.lib.py import ethtool_ret, onoff, wait_for_link, wait_for_aneg, \
+        forced_link_settings
 
     __all__ = ["NetNS", "NetNSEnter", "NetdevSimDev", "UserNetNS",
                "EthtoolFamily", "NetdevFamily", "NetshaperFamily",
@@ -49,7 +51,9 @@ try:
                "ksft_ne", "ksft_not_in", "ksft_raises", "ksft_true", "ksft_gt",
                "ksft_not_none", "ksft_not_none",
                "NetDrvEnv", "NetDrvEpEnv", "NetDrvContEnv", "GenerateTraffic",
-               "Remote", "Iperf3Runner"]
+               "Remote", "Iperf3Runner",
+               "ethtool_ret", "onoff", "wait_for_link", "wait_for_aneg",
+               "require_link_autoneg", "forced_link_settings"]
 except ModuleNotFoundError as e:
     print("Failed importing `net` library from kernel sources")
     print(str(e))
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..35cf47f4a742
--- /dev/null
+++ b/tools/testing/selftests/drivers/net/hw/pause.py
@@ -0,0 +1,353 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+
+"""
+Driver-related behavior tests for Pause-based Flow Control.
+"""
+
+import errno
+
+from lib.py import (
+    EthtoolFamily,
+    KsftFailEx,
+    KsftNamedVariant,
+    KsftSkipEx,
+    NetDrvEpEnv,
+    cmd,
+    defer,
+    ethtool,
+    ethtool_ret,
+    forced_link_settings,
+    ip,
+    ksft_disruptive,
+    ksft_eq,
+    ksft_exit,
+    ksft_in,
+    ksft_not_in,
+    ksft_pr,
+    ksft_run,
+    ksft_variants,
+    onoff,
+    wait_for_aneg,
+    wait_for_link,
+)
+
+# 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"]}},
+}
+
+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()
+]
+
+def _ethtool_pause_use_to_linkmodes(use) -> list[str]:
+    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) -> int:
+    """ set pauseparams : ethtool -A
+
+    Raise an error if the return is not 0 or EOPNOTSUPP
+
+    :param cfg: test config
+    :param rx: rx pause enabled or disabled
+    :param tx: tx pause enabled or disabled
+    :param aneg: pause autoneg enabled or disabled
+    :returns: return code of the ethtool command
+    """
+    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)
+
+    return ret
+
+def get_local_pauseparams(cfg) -> tuple[int, bool, bool, bool]:
+    """ get pauseparams : ethtool -a
+
+    Raise an error if the return is not 0 or EOPNOTSUPP
+
+    :param cfg: test config
+    :returns: tuple containing :
+              - return code of the ethtool command,
+              - rx status,
+              - tx status,
+              - aneg status
+    """
+    return ethtool_ret(f"-a {cfg.ifname}", is_get=True)
+
+def get_local_pause_supported(cfg) -> tuple[int, list[str]]:
+    """ get the supported linkmodes on the local device
+
+    :param cfg: test config
+    :returns: tuple containing :
+              - return code of the ethtool command
+              - list of linkmodes
+    """
+    ret, data = ethtool_ret(f"{cfg.ifname}")
+    if ret != 0:
+        raise KsftFailEx(f"ethtool {cfg.ifname} failed: {ret}")
+
+    return ret, _ethtool_pause_use_to_linkmodes(data["supported-pause-frame-use"])
+
+def get_local_pause_advertising(cfg) -> tuple[int, list[str]]:
+    """ get the advertised linkmodes on the local device
+
+    :param cfg: test config
+    :returns: tuple containing :
+              - return code of the ethtool command
+              - list of linkmodes
+    """
+    ret, data = ethtool_ret(f"{cfg.ifname}")
+    if ret != 0:
+        raise KsftFailEx(f"ethtool {cfg.ifname} failed: {ret}")
+
+    return ret, _ethtool_pause_use_to_linkmodes(data["advertised-pause-frame-use"])
+
+def require_pause_supported_allof(cfg, linkmodes) -> None:
+    """ Checks if the local device supports the passed linkmodes
+
+    :param cfg: test config
+    :param linkmodes: modes to test
+    """
+    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(ret, linkmodes, supported, note) -> None:
+    """ Whether ethtool -A had to work or to be refused, given what the local
+        device supports
+
+    :param ret: return value from ethtool -A
+    :param linkmodes: Pause / Asym modes corresponding to the set pause params
+    :param supported: Supported Pause / Asym
+    :param note: Message to print
+    """
+    # If supported is empty, ethtool -A must return -EOPNOTSUPP
+    if not supported :
+        ksft_eq(ret, errno.EOPNOTSUPP, note)
+    elif set(linkmodes).issubset(set(supported)) :
+        # The configured pauseparams are supposed to be supported,
+        #ethtool -A must have worked.
+        ksft_eq(ret, 0, note)
+    else :
+        # We tried to configure parameters that aren't supporteed,
+        # ethtool -A must have failed.
+        ksft_in(ret, (errno.EOPNOTSUPP, errno.EINVAL), note)
+
+def pause_setup(cfg) -> None:
+    """ The starting conditions every test here counts on, restored on exit:
+        - both ports admin up
+        - link autoneg on on both sides ifsupported
+        - link actually up (carrier on)
+
+    :param cfg: test config
+    """
+
+    # 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'])}", fail=False)
+
+    # Get init link parameters
+    link = ethtool(f"{cfg.ifname}", json=True)[0]
+    if link["auto-negotiation"]:
+        defer(cmd, f"ethtool -s {cfg.ifname} autoneg on", fail=False)
+    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()}", fail=False)
+
+    # 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'])}",
+              fail=False, host=cfg.remote)
+
+    # Get remote link params
+    link = ethtool(f"{cfg.remote_ifname}", json=True, host=cfg.remote)[0]
+    if link["auto-negotiation"]:
+        defer(cmd, f"ethtool -s {cfg.remote_ifname} autoneg on", fail=False,
+              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()}",
+              fail=False, 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 to become up on both ends
+    if not wait_for_link(cfg):
+        raise KsftFailEx("No link before the test")
+
+# Pause support : Supported linkmodes vs ability to set/get pauseparams
+@ksft_variants(pauseparams_variants)
+@ksft_disruptive
+def pause_test_support(cfg, pauseparams) -> None:
+    """ 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.
+
+    The sequence runs with link autoneg on, then with the link forced
+    (ethtool -s ethX autoneg off): the pause params are accepted or rejected
+    the same way in both cases, and both with pause autoneg off and on.
+    """
+    rx = onoff(pauseparams["rx"])
+    tx = onoff(pauseparams["tx"])
+    linkmodes = pauseparams["linkmodes"]
+
+    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:
+        ret, _ = ethtool_ret(f"-A {cfg.ifname} rx {rx} tx {tx} autoneg off",
+                             is_get = False)
+        expect_pauseparams_set(ret, linkmodes, supported, "link autoneg on")
+
+        ret, _ = ethtool_ret(f"-A {cfg.ifname} rx {rx} tx {tx} autoneg on",
+                             is_get = False)
+        expect_pauseparams_set(ret, linkmodes, 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
+
+    ret, _ = ethtool_ret(f"-A {cfg.ifname} rx {rx} tx {tx} autoneg off",
+                         is_get = False)
+    expect_pauseparams_set(ret, linkmodes, supported, "link autoneg off")
+
+    ret, _ = ethtool_ret(f"-A {cfg.ifname} rx {rx} tx {tx} autoneg on",
+                         is_get = False)
+    expect_pauseparams_set(ret, linkmodes, supported, "link autoneg off")
+
+@ksft_variants(pauseparams_variants)
+@ksft_disruptive
+def pause_advertising_test(cfg, pauseparams) -> None:
+    """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.
+    ret = wait_for_aneg(cfg)
+    ksft_eq(ret, True)
+
+    _, 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() -> None:
+    """ The hardware pause tests, on the interface the env names """
+    with NetDrvEpEnv(__file__, nsim_test=False) as cfg:
+        cfg.ethnl = EthtoolFamily()
+        ksft_run([pause_test_support,
+                  pause_advertising_test,
+                  ],
+                 args=(cfg, ))
+    ksft_exit()
+
+if __name__ == "__main__":
+    main()
diff --git a/tools/testing/selftests/drivers/net/lib/py/__init__.py b/tools/testing/selftests/drivers/net/lib/py/__init__.py
index ee903bcf3207..6efb635c5bfa 100644
--- a/tools/testing/selftests/drivers/net/lib/py/__init__.py
+++ b/tools/testing/selftests/drivers/net/lib/py/__init__.py
@@ -50,9 +50,13 @@ try:
     from .env import NetDrvEnv, NetDrvEpEnv, NetDrvContEnv
     from .load import GenerateTraffic, Iperf3Runner
     from .remote import Remote
+    from .ethtool import ethtool_ret, onoff, wait_for_link, wait_for_aneg, \
+        forced_link_settings
 
     __all__ += ["NetDrvEnv", "NetDrvEpEnv", "NetDrvContEnv", "GenerateTraffic",
-                "Remote", "Iperf3Runner"]
+                "Remote", "Iperf3Runner",
+                "ethtool_ret", "onoff", "wait_for_link", "wait_for_aneg",
+                "forced_link_settings"]
 except ModuleNotFoundError as e:
     print("Failed importing `net` library from kernel sources")
     print(str(e))
diff --git a/tools/testing/selftests/drivers/net/lib/py/ethtool.py b/tools/testing/selftests/drivers/net/lib/py/ethtool.py
new file mode 100644
index 000000000000..438bf08ccab7
--- /dev/null
+++ b/tools/testing/selftests/drivers/net/lib/py/ethtool.py
@@ -0,0 +1,133 @@
+# SPDX-License-Identifier: GPL-2.0
+
+"""
+Ethtool and link management helpers
+"""
+
+import errno
+import json
+import os
+import time
+
+from lib.py import cmd, ethtool
+
+_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}", fail=False, host=host)
+
+    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 onoff(val) -> str:
+    """ "on" or "off", the way ethtool spells a boolean """
+    return "on" if val else "off"
+
+def wait_for_aneg(cfg, link_drop=False, timeout=15) -> bool:
+    """ Wait for a renegotiation to complete.
+
+    :param cfg: test config
+    :param link_drop: Set to true if the link HAS to flap.
+    :returns: True if link is UP, False if timeout
+    """
+    deadline = time.monotonic() + timeout
+
+    # Link has 2 seconds to come back up
+    restart_by = time.monotonic() + 2
+
+    # The link may still be up for a short while when we trigger an autoneg
+    # restart, we need to wait for it to drop, then come back up again
+    while time.monotonic() < deadline:
+        if not ethtool(f"{cfg.ifname}", json=True)[0]["link-detected"]:
+            return wait_for_link(cfg)
+
+        if not link_drop and time.monotonic() > restart_by:
+            return wait_for_link(cfg)
+
+        time.sleep(0.1)
+
+    return False
+
+def wait_for_link_local(cfg, timeout=15) -> bool:
+    """ Wait for the local link to be up.
+
+    :param cfg: test config
+    :returns: True if link is UP, False if timeout
+    """
+    deadline = time.monotonic() + timeout
+
+    while time.monotonic() < deadline:
+        link = ethtool(f"{cfg.ifname}", json=True)[0]["link-detected"]
+        if link:
+            return True
+
+        time.sleep(0.1)
+
+    return False
+
+def wait_for_link_remote(cfg, timeout=15) -> bool:
+    """ Wait for the far end of the link to be up.
+
+    :param cfg: test config
+    :returns: True if link is UP, False if timeout
+    """
+    deadline = time.monotonic() + timeout
+
+    while time.monotonic() < deadline:
+        link = ethtool(f"{cfg.remote_ifname}", json=True,
+                       host=cfg.remote)[0]["link-detected"]
+        if link:
+            return True
+
+        time.sleep(0.1)
+
+    return False
+
+def wait_for_link(cfg) -> bool:
+    """ Wait for both ends of the link to be up.
+
+    :param cfg: test config
+    :returns: True if link is UP, False if timeout
+    """
+    if not wait_for_link_local(cfg):
+        return False
+
+    # Local link is UP, we shouldn't have to wait for a whole 8 seconds for
+    # the remote to report link up, let's wait a bit less
+    return wait_for_link_remote(cfg, timeout = 3)
+
+def forced_link_settings(cfg) -> str:
+    """ 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.
+
+    :param cfg: test config
+    """
+    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()}"
-- 
2.55.0


^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH net-next 2/3] selftests: drv-net: pause: tests against a controllable link partner
  2026-09-20 16:47 [PATCH net-next 0/3] selftests: drv-net: Introduce flow-control selftest Maxime Chevallier (Netdev Foundation)
  2026-09-20 16:47 ` [PATCH net-next 1/3] selftests: drv-net: Introduce a selftest for ethtool flow control Maxime Chevallier (Netdev Foundation)
@ 2026-09-20 16:47 ` Maxime Chevallier (Netdev Foundation)
  2026-09-20 16:47 ` [PATCH net-next 2/3] selftests: drv-net: pause: Validate the pause autonegotiation with a partner Maxime Chevallier (Netdev Foundation)
                   ` (2 subsequent siblings)
  4 siblings, 0 replies; 6+ messages in thread
From: Maxime Chevallier (Netdev Foundation) @ 2026-09-20 16:47 UTC (permalink / raw)
  To: Andrew Lunn, Jakub Kicinski, davem, 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, netdev, linux-kernel, linux-doc

Pause autonegotiation behaves differently than the regular link params
autonegotiation, in that the user intention may differ from the
negotiated pause parameters.

Introduce a test that validates we're correctly resolving pause
parameters according to 802.3.

The validation is done based on what the local interface reports in
terms of advertised and lp_advertised linkmodes.

If the remote can express what it sees in terms of its own advertised
and lp_advertised modes, validate the autoneg results on the peer as
well.

Signed-off-by: Maxime Chevallier (Netdev Foundation) <maxime.chevallier@bootlin.com>
---
 .../drivers/net/hw/lib/py/__init__.py         |   3 +-
 .../testing/selftests/drivers/net/hw/pause.py | 336 ++++++++++++++++++
 .../selftests/drivers/net/lib/py/__init__.py  |   3 +-
 .../selftests/drivers/net/lib/py/ethtool.py   |  60 +++-
 4 files changed, 399 insertions(+), 3 deletions(-)

diff --git a/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py b/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py
index c79dacf3bfdf..b23f53bcd3b0 100644
--- a/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py
+++ b/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py
@@ -34,6 +34,7 @@ try:
     from drivers.net.lib.py import GenerateTraffic, Remote, Iperf3Runner
     from drivers.net.lib.py import NetDrvEnv, NetDrvEpEnv, NetDrvContEnv
     from drivers.net.lib.py import ethtool_ret, onoff, wait_for_link, wait_for_aneg, \
+        controllable_lp, require_controllable_lp, \
         forced_link_settings
 
     __all__ = ["NetNS", "NetNSEnter", "NetdevSimDev", "UserNetNS",
@@ -52,7 +53,7 @@ try:
                "ksft_not_none", "ksft_not_none",
                "NetDrvEnv", "NetDrvEpEnv", "NetDrvContEnv", "GenerateTraffic",
                "Remote", "Iperf3Runner",
-               "ethtool_ret", "onoff", "wait_for_link", "wait_for_aneg",
+               "ethtool_ret", "onoff", "wait_for_link", "wait_for_aneg", "controllable_lp", "require_controllable_lp",
                "require_link_autoneg", "forced_link_settings"]
 except ModuleNotFoundError as e:
     print("Failed importing `net` library from kernel sources")
diff --git a/tools/testing/selftests/drivers/net/hw/pause.py b/tools/testing/selftests/drivers/net/hw/pause.py
index 35cf47f4a742..e291c7d8583e 100755
--- a/tools/testing/selftests/drivers/net/hw/pause.py
+++ b/tools/testing/selftests/drivers/net/hw/pause.py
@@ -14,6 +14,7 @@ from lib.py import (
     KsftSkipEx,
     NetDrvEpEnv,
     cmd,
+    controllable_lp,
     defer,
     ethtool,
     ethtool_ret,
@@ -28,6 +29,7 @@ from lib.py import (
     ksft_run,
     ksft_variants,
     onoff,
+    require_controllable_lp,
     wait_for_aneg,
     wait_for_link,
 )
@@ -57,6 +59,11 @@ def _ethtool_pause_use_to_linkmodes(use) -> list[str]:
     else:
         return []
 
+def pause_to_linkmodes(rx, tx) -> list[str]:
+    """ Convert rx/tx pauseparams to the corresponding linkmodes
+    """
+    return pauseparams_to_linkmodes[rx][tx]["linkmodes"]
+
 def set_local_pauseparams(cfg, rx, tx, aneg) -> int:
     """ set pauseparams : ethtool -A
 
@@ -92,6 +99,45 @@ def get_local_pauseparams(cfg) -> tuple[int, bool, bool, bool]:
     """
     return ethtool_ret(f"-a {cfg.ifname}", is_get=True)
 
+def set_peer_pauseparams(cfg, rx, tx, aneg) -> int:
+    """ set pauseparams : ethtool -A
+
+    Raise an error if the return is not 0 or EOPNOTSUPP
+
+    :param cfg: test config
+    :param rx: rx pause enabled or disabled
+    :param tx: tx pause enabled or disabled
+    :param aneg: pause autoneg enabled or disabled
+    :returns: return code of the ethtool command
+    """
+    rx_param = onoff(rx)
+    tx_param = onoff(tx)
+    aneg_param = onoff(aneg)
+
+    ret, _ = ethtool_ret(
+                         f"-A {cfg.remote_ifname} rx {rx_param} tx {tx_param} autoneg {aneg_param}",
+                         is_get = False, host=cfg.remote)
+
+    if ret != 0:
+        raise KsftSkipEx(f"Can't set pauseparams on peer: {errno.errorcode.get(ret, ret)}")
+
+    return ret
+
+def get_peer_pauseparams(cfg) -> tuple[int, bool, bool, bool]:
+    """ get pauseparams : ethtool -a
+
+    Raise an error if the return is not 0 or EOPNOTSUPP
+
+    :param cfg: test config
+    :returns: tuple containing :
+              - return code of the ethtool command,
+              - rx status,
+              - tx status,
+              - aneg status
+    """
+    return ethtool_ret(f"-a {cfg.remote_ifname}", is_get=True,
+                       host=cfg.remote)
+
 def get_local_pause_supported(cfg) -> tuple[int, list[str]]:
     """ get the supported linkmodes on the local device
 
@@ -120,6 +166,82 @@ def get_local_pause_advertising(cfg) -> tuple[int, list[str]]:
 
     return ret, _ethtool_pause_use_to_linkmodes(data["advertised-pause-frame-use"])
 
+def get_local_pause_lp_advertising(cfg) -> tuple[int, list[str]]:
+    """ get the lp_advertised linkmodes on the local device
+
+    Raise an error if the return is not 0 or EOPNOTSUPP
+    Prints a warning if the local device doesn't report lp_advertising
+
+    :param cfg: test config
+    :returns: tuple containing :
+              - return code of the ethtool command
+              - list of linkmodes
+    """
+    ret, data = ethtool_ret(f"{cfg.ifname}")
+    if ret != 0:
+        raise KsftFailEx(f"ethtool {cfg.ifname} failed: {errno.errorcode.get(ret, ret)}")
+
+    if "link-partner-advertised-pause-frame-use" in data:
+        return ret, _ethtool_pause_use_to_linkmodes(data["link-partner-advertised-pause-frame-use"])
+    else:
+        ksft_pr(f"Warning: {cfg.ifname} does not report the LP's advertising")
+        return errno.EOPNOTSUPP, None
+
+def get_peer_pause_supported(cfg) -> tuple[int, list[str]]:
+    """ get the supported linkmodes on the link partner
+
+    returns ENODEV if there's no LP
+
+    :param cfg: test config
+    :returns: tuple containing :
+              - return code of the ethtool command
+              - list of linkmodes
+    """
+    ret, data = ethtool_ret(f"{cfg.remote_ifname}", host = cfg.remote)
+    if ret != 0:
+        raise KsftFailEx(f"ethtool {cfg.remote_ifname} failed: {errno.errorcode.get(ret, ret)}")
+
+    return ret, _ethtool_pause_use_to_linkmodes(data["supported-pause-frame-use"])
+
+
+def get_peer_pause_advertising(cfg) -> tuple[int, list[str]]:
+    """ get the advertised linkmodes on the link partner
+
+    returns ENODEV if there's no LP
+
+    :param cfg: test config
+    :returns: tuple containing :
+              - return code of the ethtool command
+              - list of linkmodes
+    """
+    ret, data = ethtool_ret(f"{cfg.remote_ifname}", host = cfg.remote)
+    if ret != 0:
+        raise KsftFailEx(f"ethtool {cfg.remote_ifname} failed: {errno.errorcode.get(ret, ret)}")
+
+    return ret, _ethtool_pause_use_to_linkmodes(data["advertised-pause-frame-use"])
+
+def get_peer_pause_lp_advertising(cfg) -> tuple[int, list[str]]:
+    """ get the lp_advertised linkmodes on the link partner
+
+    Raise an error if the return is not 0 or EOPNOTSUPP
+    returns ENODEV if there's no LP
+    Prints a warning if LP is present but doesn't report lp_advertising
+
+    :param cfg: test config
+    :returns: tuple containing :
+              - return code of the ethtool command
+              - list of linkmodes
+    """
+    ret, data = ethtool_ret(f"{cfg.remote_ifname}", host = cfg.remote)
+    if ret != 0:
+        raise KsftFailEx(f"ethtool {cfg.remote_ifname} failed: {errno.errorcode.get(ret, ret)}")
+
+    if "link-partner-advertised-pause-frame-use" in data:
+        return ret, _ethtool_pause_use_to_linkmodes(data["link-partner-advertised-pause-frame-use"])
+    else:
+        ksft_pr(f"Warning: {cfg.remote_ifname} does not report the LP's advertising")
+        return errno.EOPNOTSUPP, None
+
 def require_pause_supported_allof(cfg, linkmodes) -> None:
     """ Checks if the local device supports the passed linkmodes
 
@@ -135,6 +257,34 @@ def require_pause_supported_allof(cfg, linkmodes) -> None:
         if lm not in pause_support:
             raise KsftSkipEx(f"Local device doesn't support {lm}")
 
+def require_peer_pause_supported_anyof(cfg, linkmodes) -> None:
+    """ Checks if the remote device supports the passed linkmodes
+    """
+
+    ret, _ = get_peer_pauseparams(cfg)
+    if ret != 0:
+        raise KsftSkipEx("Remote device doesn't allow getting pauseparams")
+
+    _, pause_support = get_peer_pause_supported(cfg)
+    for lm in linkmodes:
+        if lm in pause_support:
+            return
+
+    raise KsftSkipEx(f"Local device doesn't support any of {linkmodes}")
+
+def require_peer_pause_supported_allof(cfg, linkmodes) -> None:
+    """ Checks if the remote device supports the passed linkmodes
+    """
+
+    ret, _ = get_peer_pauseparams(cfg)
+    if ret != 0:
+        raise KsftSkipEx("Remote device doesn't allow getting pauseparams")
+
+    _, pause_support = get_peer_pause_supported(cfg)
+    for lm in linkmodes:
+        if lm not in pause_support:
+            raise KsftSkipEx(f"Remote device doesn't support {lm}")
+
 def expect_pauseparams_set(ret, linkmodes, supported, note) -> None:
     """ Whether ethtool -A had to work or to be refused, given what the local
         device supports
@@ -311,6 +461,7 @@ def pause_advertising_test(cfg, pauseparams) -> None:
 
     require_pause_supported_allof(cfg, pauseparams["linkmodes"])
     pause_setup(cfg)
+    lp = controllable_lp(cfg)
 
     tx = pauseparams["tx"]
     rx = pauseparams["rx"]
@@ -338,6 +489,190 @@ def pause_advertising_test(cfg, pauseparams) -> None:
         ksft_not_in(mode, linkmodes,
                     f"rx {rx} tx {tx} aneg on must not advertise {not_adv}")
 
+    if not lp:
+        return
+
+    returncode, remote_linkmodes = get_peer_pause_lp_advertising(cfg)
+    if returncode == errno.EOPNOTSUPP:
+        return
+
+    for mode in adv:
+        ksft_in(mode, remote_linkmodes, f"PHY does not advertise {adv}")
+
+    for mode in not_adv:
+        ksft_not_in(mode, remote_linkmodes,
+                    f"PHY incorrectly advertises {not_adv}")
+
+
+# Pause autonegotiation resolution : Resolved pause settings vs configured
+# pauseparams on local device and link partner
+@ksft_variants([
+    # We advertise nothing, all off
+    KsftNamedVariant("local rx off tx off, remote rx off tx off",
+        {"rx": 0, "tx": 0, "lp_rx": 0, "lp_tx": 0, "neg_rx": 0, "neg_tx": 0}),
+
+    # We advertise nothing, all off
+    KsftNamedVariant("local rx off tx off, remote rx off tx on",
+        {"rx": 0, "tx": 0, "lp_rx": 0, "lp_tx": 1, "neg_rx": 0, "neg_tx": 0}),
+
+    # We advertise nothing, all off
+    KsftNamedVariant("local rx off tx off, remote rx on tx off",
+        {"rx": 0, "tx": 0, "lp_rx": 1, "lp_tx": 0, "neg_rx": 0, "neg_tx": 0}),
+
+    # We advertise nothing, all off
+    KsftNamedVariant("local rx off tx off, remote rx on tx on",
+        {"rx": 0, "tx": 0, "lp_rx": 1, "lp_tx": 1, "neg_rx": 0, "neg_tx": 0}),
+
+    # LP advertises nothing, all off
+    KsftNamedVariant("local rx off tx on, remote rx off tx off",
+        {"rx": 0, "tx": 1, "lp_rx": 0, "lp_tx": 0, "neg_rx": 0, "neg_tx": 0}),
+
+    # We advertise Asym, LP advertises Asym, all off
+    KsftNamedVariant("local rx off tx on, remote rx off tx on",
+        {"rx": 0, "tx": 1, "lp_rx": 0, "lp_tx": 1, "neg_rx": 0, "neg_tx": 0}),
+
+    # We advertise Asym, LP advertises Pause + Asym, tx on
+    KsftNamedVariant("local rx off tx on, remote rx on tx off",
+        {"rx": 0, "tx": 1, "lp_rx": 1, "lp_tx": 0, "neg_rx": 0, "neg_tx": 1}),
+
+    # Tricky case :
+    # We advertise Asym, LP advertises Pause, resolves to all off
+    KsftNamedVariant("local rx off tx on, remote rx on tx on",
+        {"rx": 0, "tx": 1, "lp_rx": 1, "lp_tx": 1, "neg_rx": 0, "neg_tx": 0}),
+
+    # LP advertises nothing, all off
+    KsftNamedVariant("local rx on tx off, remote rx off tx off",
+        {"rx": 1, "tx": 0, "lp_rx": 0, "lp_tx": 0, "neg_rx": 0, "neg_tx": 0}),
+
+    # We advertise Pause + Asym , LP advertises Asym, rx on
+    KsftNamedVariant("local rx on tx off, remote rx off tx on",
+        {"rx": 1, "tx": 0, "lp_rx": 0, "lp_tx": 1, "neg_rx": 1, "neg_tx": 0}),
+
+    # Also tricky: Only rx enabled on both ends, but we negotiate rx/tx
+    # We advertise Pause + Asym, LP advertises Pause + Asym, all on
+    KsftNamedVariant("local rx on tx off, remote rx on tx off",
+        {"rx": 1, "tx": 0, "lp_rx": 1, "lp_tx": 0, "neg_rx": 1, "neg_tx": 1}),
+
+    # We advertise Pause + Asym, LP advertises Pause, all on
+    KsftNamedVariant("local rx on tx off, remote rx on tx on",
+        {"rx": 1, "tx": 0, "lp_rx": 1, "lp_tx": 1, "neg_rx": 1, "neg_tx": 1}),
+
+    # LP advertises nothing, all off
+    KsftNamedVariant("local rx on tx on, remote rx off tx off",
+        {"rx": 1, "tx": 1, "lp_rx": 0, "lp_tx": 0, "neg_rx": 0, "neg_tx": 0}),
+
+    # Tricky case :
+    # We advertise Pause, LP advertises Asym, resolves to all off
+    KsftNamedVariant("local rx on tx on, remote rx off tx on",
+        {"rx": 1, "tx": 1, "lp_rx": 0, "lp_tx": 1, "neg_rx": 0, "neg_tx": 0}),
+
+    # We advertise Pause, LP advertises Pause + Asym, all on
+    KsftNamedVariant("local rx on tx on, remote rx on tx off",
+        {"rx": 1, "tx": 1, "lp_rx": 1, "lp_tx": 0, "neg_rx": 1, "neg_tx": 1}),
+
+    # We advertise Pause, LP advertises Pause, all on
+    KsftNamedVariant("local rx on tx on, remote rx on tx on",
+        {"rx": 1, "tx": 1, "lp_rx": 1, "lp_tx": 1, "neg_rx": 1, "neg_tx": 1}),
+])
+@ksft_disruptive
+def pause_aneg_resolution(cfg, settings) -> None:
+    """ Verify that rx and tx pause parameters are negotiated according to 802.3
+
+    802.3 dictates the rules for pause negotiation, all 16 cases are tested, one
+    for each combination of Pause and Asym_Pause advertising on the local device
+    and the link-partner.
+
+    This test also verifies that the peer resolved the parameters correctly,
+    to ensure the negotiation is triggered correctly.
+
+    Failing this test can happen if :
+     - The MAC accepts the pause parameters but doesn't trigger a link
+       renegotiation
+     - that the PHY driver manually overwrites the Pause negotiation result
+     - that the MAC driver ignores the Pause resolution and sets its own
+       pause parameters regardless
+    """
+    expected_local_rx = settings["neg_rx"]
+    expected_local_tx = settings["neg_tx"]
+
+    required_local_linkmodes = pause_to_linkmodes(settings["rx"],
+                                                  settings["tx"])
+    required_remote_linkmodes = pause_to_linkmodes(settings["lp_rx"],
+                                                   settings["lp_tx"])
+
+    require_pause_supported_allof(cfg, required_local_linkmodes)
+    require_peer_pause_supported_allof(cfg, required_remote_linkmodes)
+    require_controllable_lp(cfg)
+    pause_setup(cfg)
+
+    # There's symmetry between local device and LP on pause negotiation:
+    # - if local resolves all off or all on, LP must resolve the same
+    # - if local resolves RX only, remote must resolve to TX only
+    # - if local resolves TX only, remote must resolve to RX only
+    if expected_local_rx == expected_local_tx:
+        expected_lp_rx = expected_local_rx
+        expected_lp_tx = expected_local_tx
+    else:
+        expected_lp_rx = expected_local_tx
+        expected_lp_tx = expected_local_rx
+
+    # Set pauseparams
+    ret = set_local_pauseparams(cfg, settings["rx"], settings["tx"], True)
+    if ret == errno.EOPNOTSUPP:
+        raise KsftSkipEx(f"RX {settings['rx']} TX {settings['tx']} not supported")
+
+    ksft_eq(wait_for_aneg(cfg), True)
+
+    set_peer_pauseparams(cfg, settings["lp_rx"], settings["lp_tx"], True)
+
+    # Wait for link to re-negotiate
+    ret = wait_for_aneg(cfg)
+
+    # Fail if it doesn't
+    ksft_eq(ret, True)
+
+    if get_local_pause_lp_advertising(cfg)[0] != 0:
+        raise KsftSkipEx("Local device doesn't report the LP's advertising")
+
+    ret, local_pauseparams = get_local_pauseparams(cfg)
+    if ret != 0 or "negotiated" not in local_pauseparams:
+        raise KsftSkipEx("Local device doesn't report the negotiated pause params")
+
+    # check adv
+    _, linkmodes = get_local_pause_advertising(cfg)
+    for mode in required_local_linkmodes:
+        ksft_in(mode, linkmodes,
+                f"local rx {settings['rx']} tx {settings['tx']} must advertise "
+                f"{required_local_linkmodes}")
+
+    _, linkmodes = get_peer_pause_advertising(cfg)
+    for mode in required_remote_linkmodes:
+        ksft_in(mode, linkmodes,
+                f"remote rx {settings['lp_rx']} tx {settings['lp_tx']} must advertise "
+                f"{required_remote_linkmodes}")
+
+    # check lp_adv if available
+    _, linkmodes = get_local_pause_lp_advertising(cfg)
+    for mode in required_remote_linkmodes:
+        ksft_in(mode, linkmodes,
+                f"local lp_adv must show the remote's {required_remote_linkmodes}")
+
+    # check lp_adv on remote
+    ret, linkmodes = get_peer_pause_lp_advertising(cfg)
+    if ret == 0:
+        for mode in required_local_linkmodes:
+            ksft_in(mode, linkmodes,
+                    f"remote lp_adv must show our {required_local_linkmodes}")
+
+    # Check resolution
+    _, local_pauseparams = get_local_pauseparams(cfg)
+    ksft_eq(local_pauseparams["negotiated"]["rx"], expected_local_rx)
+    ksft_eq(local_pauseparams["negotiated"]["tx"], expected_local_tx)
+
+    ret, remote_pauseparams = get_peer_pauseparams(cfg)
+    if ret == 0 and "negotiated" in remote_pauseparams:
+        ksft_eq(remote_pauseparams["negotiated"]["rx"], expected_lp_rx)
+        ksft_eq(remote_pauseparams["negotiated"]["tx"], expected_lp_tx)
 
 def main() -> None:
     """ The hardware pause tests, on the interface the env names """
@@ -345,6 +680,7 @@ def main() -> None:
         cfg.ethnl = EthtoolFamily()
         ksft_run([pause_test_support,
                   pause_advertising_test,
+                  pause_aneg_resolution,
                   ],
                  args=(cfg, ))
     ksft_exit()
diff --git a/tools/testing/selftests/drivers/net/lib/py/__init__.py b/tools/testing/selftests/drivers/net/lib/py/__init__.py
index 6efb635c5bfa..0486405c5c70 100644
--- a/tools/testing/selftests/drivers/net/lib/py/__init__.py
+++ b/tools/testing/selftests/drivers/net/lib/py/__init__.py
@@ -51,11 +51,12 @@ try:
     from .load import GenerateTraffic, Iperf3Runner
     from .remote import Remote
     from .ethtool import ethtool_ret, onoff, wait_for_link, wait_for_aneg, \
+        controllable_lp, require_controllable_lp, \
         forced_link_settings
 
     __all__ += ["NetDrvEnv", "NetDrvEpEnv", "NetDrvContEnv", "GenerateTraffic",
                 "Remote", "Iperf3Runner",
-                "ethtool_ret", "onoff", "wait_for_link", "wait_for_aneg",
+                "ethtool_ret", "onoff", "wait_for_link", "wait_for_aneg", "controllable_lp", "require_controllable_lp",
                 "forced_link_settings"]
 except ModuleNotFoundError as e:
     print("Failed importing `net` library from kernel sources")
diff --git a/tools/testing/selftests/drivers/net/lib/py/ethtool.py b/tools/testing/selftests/drivers/net/lib/py/ethtool.py
index 438bf08ccab7..43e197ac12e5 100644
--- a/tools/testing/selftests/drivers/net/lib/py/ethtool.py
+++ b/tools/testing/selftests/drivers/net/lib/py/ethtool.py
@@ -9,7 +9,7 @@ import json
 import os
 import time
 
-from lib.py import cmd, ethtool
+from lib.py import KsftSkipEx, cmd, ethtool, ip, ksft_pr
 
 _strerrors = {os.strerror(e): e for e in errno.errorcode}
 
@@ -117,6 +117,64 @@ def wait_for_link(cfg) -> bool:
     # the remote to report link up, let's wait a bit less
     return wait_for_link_remote(cfg, timeout = 3)
 
+def controllable_lp(cfg) -> bool:
+    """ Whether the remote interface is the link partner of the local one.
+
+    For low level ethtool tests, we need to have the remote directly connected
+    to the local host (i.e. not through a switch).
+
+    This is tested by taking the local link down and checking that the
+    remote's link drops.
+
+    :param cfg: test config
+    :returns: True if the remote is our link partner
+    """
+    known = getattr(cfg, "lp_controllable", None)
+    if known is not None:
+        return known
+
+    # Set both ends up, wait for up
+    ip(f"link set {cfg.ifname} up")
+    ip(f"link set {cfg.remote_ifname} up", host=cfg.remote)
+
+    # No link established
+    if not wait_for_link(cfg):
+        ksft_pr(f"{cfg.remote_ifname} is not the link partner: no link with both ends up")
+        cfg.lp_controllable = False
+        return False
+
+    ip(f"link set {cfg.ifname} down")
+
+    deadline = time.monotonic() + 3
+    dropped = False
+    while time.monotonic() < deadline and not dropped:
+        dropped = not ethtool(f"{cfg.remote_ifname}", json=True,
+                              host=cfg.remote)[0]["link-detected"]
+        time.sleep(0.1)
+
+    ip(f"link set {cfg.ifname} up")
+
+    if not dropped:
+        ksft_pr(f"{cfg.remote_ifname} is not the link partner: "
+                f"it kept its link through {cfg.ifname} going down")
+        cfg.lp_controllable = False
+        wait_for_link(cfg)
+        return False
+
+    cfg.lp_controllable = wait_for_link(cfg)
+    if not cfg.lp_controllable:
+        ksft_pr(f"{cfg.remote_ifname} is not the link partner: "
+                f"no link back after {cfg.ifname} came up")
+    return cfg.lp_controllable
+
+def require_controllable_lp(cfg) -> None:
+    """ Skip if the remote isn't directly connected to the local device
+
+    :param cfg: test config
+    """
+    if not controllable_lp(cfg):
+        raise KsftSkipEx(f"{cfg.remote_ifname} is not directly connected to {cfg.ifname}")
+
 def forced_link_settings(cfg) -> str:
     """ Returns a string to pass to ethtool -s with speed/duplex corresponding
         to the current settings.
-- 
2.55.0


^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH net-next 2/3] selftests: drv-net: pause: Validate the pause autonegotiation with a partner
  2026-09-20 16:47 [PATCH net-next 0/3] selftests: drv-net: Introduce flow-control selftest Maxime Chevallier (Netdev Foundation)
  2026-09-20 16:47 ` [PATCH net-next 1/3] selftests: drv-net: Introduce a selftest for ethtool flow control Maxime Chevallier (Netdev Foundation)
  2026-09-20 16:47 ` [PATCH net-next 2/3] selftests: drv-net: pause: tests against a controllable link partner Maxime Chevallier (Netdev Foundation)
@ 2026-09-20 16:47 ` Maxime Chevallier (Netdev Foundation)
  2026-09-20 16:47 ` [PATCH net-next 3/3] selftests: drv-net: pause: pause autoneg tests Maxime Chevallier (Netdev Foundation)
  2026-09-20 16:47 ` [PATCH net-next 3/3] selftests: drv-net: pause: Validate pause autoneg interactions with link autoneg Maxime Chevallier (Netdev Foundation)
  4 siblings, 0 replies; 6+ messages in thread
From: Maxime Chevallier (Netdev Foundation) @ 2026-09-20 16:47 UTC (permalink / raw)
  To: Andrew Lunn, Jakub Kicinski, davem, 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, netdev, linux-kernel, linux-doc

Pause autonegotiation behaves differently than the regular link params
autonegotiation, in that the user intention may differ from the
negotiated pause parameters.

Introduce a test that validates we're correctly resolving pause
parameters according to 802.3.

The validation is done based on what the local interface reports in
terms of advertised and lp_advertised linkmodes.

If the remote can express what it sees in terms of its own advertised
and lp_advertised modes, validate the autoneg results on the peer as
well.

Signed-off-by: Maxime Chevallier (Netdev Foundation) <maxime.chevallier@bootlin.com>
---
 .../drivers/net/hw/lib/py/__init__.py         |   3 +-
 .../testing/selftests/drivers/net/hw/pause.py | 336 ++++++++++++++++++
 .../selftests/drivers/net/lib/py/__init__.py  |   3 +-
 .../selftests/drivers/net/lib/py/ethtool.py   |  60 +++-
 4 files changed, 399 insertions(+), 3 deletions(-)

diff --git a/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py b/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py
index c79dacf3bfdf..b23f53bcd3b0 100644
--- a/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py
+++ b/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py
@@ -34,6 +34,7 @@ try:
     from drivers.net.lib.py import GenerateTraffic, Remote, Iperf3Runner
     from drivers.net.lib.py import NetDrvEnv, NetDrvEpEnv, NetDrvContEnv
     from drivers.net.lib.py import ethtool_ret, onoff, wait_for_link, wait_for_aneg, \
+        controllable_lp, require_controllable_lp, \
         forced_link_settings
 
     __all__ = ["NetNS", "NetNSEnter", "NetdevSimDev", "UserNetNS",
@@ -52,7 +53,7 @@ try:
                "ksft_not_none", "ksft_not_none",
                "NetDrvEnv", "NetDrvEpEnv", "NetDrvContEnv", "GenerateTraffic",
                "Remote", "Iperf3Runner",
-               "ethtool_ret", "onoff", "wait_for_link", "wait_for_aneg",
+               "ethtool_ret", "onoff", "wait_for_link", "wait_for_aneg", "controllable_lp", "require_controllable_lp",
                "require_link_autoneg", "forced_link_settings"]
 except ModuleNotFoundError as e:
     print("Failed importing `net` library from kernel sources")
diff --git a/tools/testing/selftests/drivers/net/hw/pause.py b/tools/testing/selftests/drivers/net/hw/pause.py
index 35cf47f4a742..e291c7d8583e 100755
--- a/tools/testing/selftests/drivers/net/hw/pause.py
+++ b/tools/testing/selftests/drivers/net/hw/pause.py
@@ -14,6 +14,7 @@ from lib.py import (
     KsftSkipEx,
     NetDrvEpEnv,
     cmd,
+    controllable_lp,
     defer,
     ethtool,
     ethtool_ret,
@@ -28,6 +29,7 @@ from lib.py import (
     ksft_run,
     ksft_variants,
     onoff,
+    require_controllable_lp,
     wait_for_aneg,
     wait_for_link,
 )
@@ -57,6 +59,11 @@ def _ethtool_pause_use_to_linkmodes(use) -> list[str]:
     else:
         return []
 
+def pause_to_linkmodes(rx, tx) -> list[str]:
+    """ Convert rx/tx pauseparams to the corresponding linkmodes
+    """
+    return pauseparams_to_linkmodes[rx][tx]["linkmodes"]
+
 def set_local_pauseparams(cfg, rx, tx, aneg) -> int:
     """ set pauseparams : ethtool -A
 
@@ -92,6 +99,45 @@ def get_local_pauseparams(cfg) -> tuple[int, bool, bool, bool]:
     """
     return ethtool_ret(f"-a {cfg.ifname}", is_get=True)
 
+def set_peer_pauseparams(cfg, rx, tx, aneg) -> int:
+    """ set pauseparams : ethtool -A
+
+    Raise an error if the return is not 0 or EOPNOTSUPP
+
+    :param cfg: test config
+    :param rx: rx pause enabled or disabled
+    :param tx: tx pause enabled or disabled
+    :param aneg: pause autoneg enabled or disabled
+    :returns: return code of the ethtool command
+    """
+    rx_param = onoff(rx)
+    tx_param = onoff(tx)
+    aneg_param = onoff(aneg)
+
+    ret, _ = ethtool_ret(
+                         f"-A {cfg.remote_ifname} rx {rx_param} tx {tx_param} autoneg {aneg_param}",
+                         is_get = False, host=cfg.remote)
+
+    if ret != 0:
+        raise KsftSkipEx(f"Can't set pauseparams on peer: {errno.errorcode.get(ret, ret)}")
+
+    return ret
+
+def get_peer_pauseparams(cfg) -> tuple[int, bool, bool, bool]:
+    """ get pauseparams : ethtool -a
+
+    Raise an error if the return is not 0 or EOPNOTSUPP
+
+    :param cfg: test config
+    :returns: tuple containing :
+              - return code of the ethtool command,
+              - rx status,
+              - tx status,
+              - aneg status
+    """
+    return ethtool_ret(f"-a {cfg.remote_ifname}", is_get=True,
+                       host=cfg.remote)
+
 def get_local_pause_supported(cfg) -> tuple[int, list[str]]:
     """ get the supported linkmodes on the local device
 
@@ -120,6 +166,82 @@ def get_local_pause_advertising(cfg) -> tuple[int, list[str]]:
 
     return ret, _ethtool_pause_use_to_linkmodes(data["advertised-pause-frame-use"])
 
+def get_local_pause_lp_advertising(cfg) -> tuple[int, list[str]]:
+    """ get the lp_advertised linkmodes on the local device
+
+    Raise an error if the return is not 0 or EOPNOTSUPP
+    Prints a warning if the local device doesn't report lp_advertising
+
+    :param cfg: test config
+    :returns: tuple containing :
+              - return code of the ethtool command
+              - list of linkmodes
+    """
+    ret, data = ethtool_ret(f"{cfg.ifname}")
+    if ret != 0:
+        raise KsftFailEx(f"ethtool {cfg.ifname} failed: {errno.errorcode.get(ret, ret)}")
+
+    if "link-partner-advertised-pause-frame-use" in data:
+        return ret, _ethtool_pause_use_to_linkmodes(data["link-partner-advertised-pause-frame-use"])
+    else:
+        ksft_pr(f"Warning: {cfg.ifname} does not report the LP's advertising")
+        return errno.EOPNOTSUPP, None
+
+def get_peer_pause_supported(cfg) -> tuple[int, list[str]]:
+    """ get the supported linkmodes on the link partner
+
+    returns ENODEV if there's no LP
+
+    :param cfg: test config
+    :returns: tuple containing :
+              - return code of the ethtool command
+              - list of linkmodes
+    """
+    ret, data = ethtool_ret(f"{cfg.remote_ifname}", host = cfg.remote)
+    if ret != 0:
+        raise KsftFailEx(f"ethtool {cfg.remote_ifname} failed: {errno.errorcode.get(ret, ret)}")
+
+    return ret, _ethtool_pause_use_to_linkmodes(data["supported-pause-frame-use"])
+
+
+def get_peer_pause_advertising(cfg) -> tuple[int, list[str]]:
+    """ get the advertised linkmodes on the link partner
+
+    returns ENODEV if there's no LP
+
+    :param cfg: test config
+    :returns: tuple containing :
+              - return code of the ethtool command
+              - list of linkmodes
+    """
+    ret, data = ethtool_ret(f"{cfg.remote_ifname}", host = cfg.remote)
+    if ret != 0:
+        raise KsftFailEx(f"ethtool {cfg.remote_ifname} failed: {errno.errorcode.get(ret, ret)}")
+
+    return ret, _ethtool_pause_use_to_linkmodes(data["advertised-pause-frame-use"])
+
+def get_peer_pause_lp_advertising(cfg) -> tuple[int, list[str]]:
+    """ get the lp_advertised linkmodes on the link partner
+
+    Raise an error if the return is not 0 or EOPNOTSUPP
+    returns ENODEV if there's no LP
+    Prints a warning if LP is present but doesn't report lp_advertising
+
+    :param cfg: test config
+    :returns: tuple containing :
+              - return code of the ethtool command
+              - list of linkmodes
+    """
+    ret, data = ethtool_ret(f"{cfg.remote_ifname}", host = cfg.remote)
+    if ret != 0:
+        raise KsftFailEx(f"ethtool {cfg.remote_ifname} failed: {errno.errorcode.get(ret, ret)}")
+
+    if "link-partner-advertised-pause-frame-use" in data:
+        return ret, _ethtool_pause_use_to_linkmodes(data["link-partner-advertised-pause-frame-use"])
+    else:
+        ksft_pr(f"Warning: {cfg.remote_ifname} does not report the LP's advertising")
+        return errno.EOPNOTSUPP, None
+
 def require_pause_supported_allof(cfg, linkmodes) -> None:
     """ Checks if the local device supports the passed linkmodes
 
@@ -135,6 +257,34 @@ def require_pause_supported_allof(cfg, linkmodes) -> None:
         if lm not in pause_support:
             raise KsftSkipEx(f"Local device doesn't support {lm}")
 
+def require_peer_pause_supported_anyof(cfg, linkmodes) -> None:
+    """ Checks if the remote device supports the passed linkmodes
+    """
+
+    ret, _ = get_peer_pauseparams(cfg)
+    if ret != 0:
+        raise KsftSkipEx("Remote device doesn't allow getting pauseparams")
+
+    _, pause_support = get_peer_pause_supported(cfg)
+    for lm in linkmodes:
+        if lm in pause_support:
+            return
+
+    raise KsftSkipEx(f"Local device doesn't support any of {linkmodes}")
+
+def require_peer_pause_supported_allof(cfg, linkmodes) -> None:
+    """ Checks if the remote device supports the passed linkmodes
+    """
+
+    ret, _ = get_peer_pauseparams(cfg)
+    if ret != 0:
+        raise KsftSkipEx("Remote device doesn't allow getting pauseparams")
+
+    _, pause_support = get_peer_pause_supported(cfg)
+    for lm in linkmodes:
+        if lm not in pause_support:
+            raise KsftSkipEx(f"Remote device doesn't support {lm}")
+
 def expect_pauseparams_set(ret, linkmodes, supported, note) -> None:
     """ Whether ethtool -A had to work or to be refused, given what the local
         device supports
@@ -311,6 +461,7 @@ def pause_advertising_test(cfg, pauseparams) -> None:
 
     require_pause_supported_allof(cfg, pauseparams["linkmodes"])
     pause_setup(cfg)
+    lp = controllable_lp(cfg)
 
     tx = pauseparams["tx"]
     rx = pauseparams["rx"]
@@ -338,6 +489,190 @@ def pause_advertising_test(cfg, pauseparams) -> None:
         ksft_not_in(mode, linkmodes,
                     f"rx {rx} tx {tx} aneg on must not advertise {not_adv}")
 
+    if not lp:
+        return
+
+    returncode, remote_linkmodes = get_peer_pause_lp_advertising(cfg)
+    if returncode == errno.EOPNOTSUPP:
+        return
+
+    for mode in adv:
+        ksft_in(mode, remote_linkmodes, f"PHY does not advertise {adv}")
+
+    for mode in not_adv:
+        ksft_not_in(mode, remote_linkmodes,
+                    f"PHY incorrectly advertises {not_adv}")
+
+
+# Pause autonegotiation resolution : Resolved pause settings vs configured
+# pauseparams on local device and link partner
+@ksft_variants([
+    # We advertise nothing, all off
+    KsftNamedVariant("local rx off tx off, remote rx off tx off",
+        {"rx": 0, "tx": 0, "lp_rx": 0, "lp_tx": 0, "neg_rx": 0, "neg_tx": 0}),
+
+    # We advertise nothing, all off
+    KsftNamedVariant("local rx off tx off, remote rx off tx on",
+        {"rx": 0, "tx": 0, "lp_rx": 0, "lp_tx": 1, "neg_rx": 0, "neg_tx": 0}),
+
+    # We advertise nothing, all off
+    KsftNamedVariant("local rx off tx off, remote rx on tx off",
+        {"rx": 0, "tx": 0, "lp_rx": 1, "lp_tx": 0, "neg_rx": 0, "neg_tx": 0}),
+
+    # We advertise nothing, all off
+    KsftNamedVariant("local rx off tx off, remote rx on tx on",
+        {"rx": 0, "tx": 0, "lp_rx": 1, "lp_tx": 1, "neg_rx": 0, "neg_tx": 0}),
+
+    # LP advertises nothing, all off
+    KsftNamedVariant("local rx off tx on, remote rx off tx off",
+        {"rx": 0, "tx": 1, "lp_rx": 0, "lp_tx": 0, "neg_rx": 0, "neg_tx": 0}),
+
+    # We advertise Asym, LP advertises Asym, all off
+    KsftNamedVariant("local rx off tx on, remote rx off tx on",
+        {"rx": 0, "tx": 1, "lp_rx": 0, "lp_tx": 1, "neg_rx": 0, "neg_tx": 0}),
+
+    # We advertise Asym, LP advertises Pause + Asym, tx on
+    KsftNamedVariant("local rx off tx on, remote rx on tx off",
+        {"rx": 0, "tx": 1, "lp_rx": 1, "lp_tx": 0, "neg_rx": 0, "neg_tx": 1}),
+
+    # Tricky case :
+    # We advertise Asym, LP advertises Pause, resolves to all off
+    KsftNamedVariant("local rx off tx on, remote rx on tx on",
+        {"rx": 0, "tx": 1, "lp_rx": 1, "lp_tx": 1, "neg_rx": 0, "neg_tx": 0}),
+
+    # LP advertises nothing, all off
+    KsftNamedVariant("local rx on tx off, remote rx off tx off",
+        {"rx": 1, "tx": 0, "lp_rx": 0, "lp_tx": 0, "neg_rx": 0, "neg_tx": 0}),
+
+    # We advertise Pause + Asym , LP advertises Asym, rx on
+    KsftNamedVariant("local rx on tx off, remote rx off tx on",
+        {"rx": 1, "tx": 0, "lp_rx": 0, "lp_tx": 1, "neg_rx": 1, "neg_tx": 0}),
+
+    # Also tricky: Only rx enabled on both ends, but we negotiate rx/tx
+    # We advertise Pause + Asym, LP advertises Pause + Asym, all on
+    KsftNamedVariant("local rx on tx off, remote rx on tx off",
+        {"rx": 1, "tx": 0, "lp_rx": 1, "lp_tx": 0, "neg_rx": 1, "neg_tx": 1}),
+
+    # We advertise Pause + Asym, LP advertises Pause, all on
+    KsftNamedVariant("local rx on tx off, remote rx on tx on",
+        {"rx": 1, "tx": 0, "lp_rx": 1, "lp_tx": 1, "neg_rx": 1, "neg_tx": 1}),
+
+    # LP advertises nothing, all off
+    KsftNamedVariant("local rx on tx on, remote rx off tx off",
+        {"rx": 1, "tx": 1, "lp_rx": 0, "lp_tx": 0, "neg_rx": 0, "neg_tx": 0}),
+
+    # Tricky case :
+    # We advertise Pause, LP advertises Asym, resolves to all off
+    KsftNamedVariant("local rx on tx on, remote rx off tx on",
+        {"rx": 1, "tx": 1, "lp_rx": 0, "lp_tx": 1, "neg_rx": 0, "neg_tx": 0}),
+
+    # We advertise Pause, LP advertises Pause + Asym, all on
+    KsftNamedVariant("local rx on tx on, remote rx on tx off",
+        {"rx": 1, "tx": 1, "lp_rx": 1, "lp_tx": 0, "neg_rx": 1, "neg_tx": 1}),
+
+    # We advertise Pause, LP advertises Pause, all on
+    KsftNamedVariant("local rx on tx on, remote rx on tx on",
+        {"rx": 1, "tx": 1, "lp_rx": 1, "lp_tx": 1, "neg_rx": 1, "neg_tx": 1}),
+])
+@ksft_disruptive
+def pause_aneg_resolution(cfg, settings) -> None:
+    """ Verify that rx and tx pause parameters are negotiated according to 802.3
+
+    802.3 dictates the rules for pause negotiation, all 16 cases are tested, one
+    for each combination of Pause and Asym_Pause advertising on the local device
+    and the link-partner.
+
+    This test also verifies that the peer resolved the parameters correctly,
+    to ensure the negotiation is triggered correctly.
+
+    Failing this test can happen if :
+     - The MAC accepts the pause parameters but doesn't trigger a link
+       renegotiation
+     - that the PHY driver manually overwrites the Pause negotiation result
+     - that the MAC driver ignores the Pause resolution and sets its own
+       pause parameters regardless
+    """
+    expected_local_rx = settings["neg_rx"]
+    expected_local_tx = settings["neg_tx"]
+
+    required_local_linkmodes = pause_to_linkmodes(settings["rx"],
+                                                  settings["tx"])
+    required_remote_linkmodes = pause_to_linkmodes(settings["lp_rx"],
+                                                   settings["lp_tx"])
+
+    require_pause_supported_allof(cfg, required_local_linkmodes)
+    require_peer_pause_supported_allof(cfg, required_remote_linkmodes)
+    require_controllable_lp(cfg)
+    pause_setup(cfg)
+
+    # There's symmetry between local device and LP on pause negotiation:
+    # - if local resolves all off or all on, LP must resolve the same
+    # - if local resolves RX only, remote must resolve to TX only
+    # - if local resolves TX only, remote must resolve to RX only
+    if expected_local_rx == expected_local_tx:
+        expected_lp_rx = expected_local_rx
+        expected_lp_tx = expected_local_tx
+    else:
+        expected_lp_rx = expected_local_tx
+        expected_lp_tx = expected_local_rx
+
+    # Set pauseparams
+    ret = set_local_pauseparams(cfg, settings["rx"], settings["tx"], True)
+    if ret == errno.EOPNOTSUPP:
+        raise KsftSkipEx(f"RX {settings['rx']} TX {settings['tx']} not supported")
+
+    ksft_eq(wait_for_aneg(cfg), True)
+
+    set_peer_pauseparams(cfg, settings["lp_rx"], settings["lp_tx"], True)
+
+    # Wait for link to re-negotiate
+    ret = wait_for_aneg(cfg)
+
+    # Fail if it doesn't
+    ksft_eq(ret, True)
+
+    if get_local_pause_lp_advertising(cfg)[0] != 0:
+        raise KsftSkipEx("Local device doesn't report the LP's advertising")
+
+    ret, local_pauseparams = get_local_pauseparams(cfg)
+    if ret != 0 or "negotiated" not in local_pauseparams:
+        raise KsftSkipEx("Local device doesn't report the negotiated pause params")
+
+    # check adv
+    _, linkmodes = get_local_pause_advertising(cfg)
+    for mode in required_local_linkmodes:
+        ksft_in(mode, linkmodes,
+                f"local rx {settings['rx']} tx {settings['tx']} must advertise "
+                f"{required_local_linkmodes}")
+
+    _, linkmodes = get_peer_pause_advertising(cfg)
+    for mode in required_remote_linkmodes:
+        ksft_in(mode, linkmodes,
+                f"remote rx {settings['lp_rx']} tx {settings['lp_tx']} must advertise "
+                f"{required_remote_linkmodes}")
+
+    # check lp_adv if available
+    _, linkmodes = get_local_pause_lp_advertising(cfg)
+    for mode in required_remote_linkmodes:
+        ksft_in(mode, linkmodes,
+                f"local lp_adv must show the remote's {required_remote_linkmodes}")
+
+    # check lp_adv on remote
+    ret, linkmodes = get_peer_pause_lp_advertising(cfg)
+    if ret == 0:
+        for mode in required_local_linkmodes:
+            ksft_in(mode, linkmodes,
+                    f"remote lp_adv must show our {required_local_linkmodes}")
+
+    # Check resolution
+    _, local_pauseparams = get_local_pauseparams(cfg)
+    ksft_eq(local_pauseparams["negotiated"]["rx"], expected_local_rx)
+    ksft_eq(local_pauseparams["negotiated"]["tx"], expected_local_tx)
+
+    ret, remote_pauseparams = get_peer_pauseparams(cfg)
+    if ret == 0 and "negotiated" in remote_pauseparams:
+        ksft_eq(remote_pauseparams["negotiated"]["rx"], expected_lp_rx)
+        ksft_eq(remote_pauseparams["negotiated"]["tx"], expected_lp_tx)
 
 def main() -> None:
     """ The hardware pause tests, on the interface the env names """
@@ -345,6 +680,7 @@ def main() -> None:
         cfg.ethnl = EthtoolFamily()
         ksft_run([pause_test_support,
                   pause_advertising_test,
+                  pause_aneg_resolution,
                   ],
                  args=(cfg, ))
     ksft_exit()
diff --git a/tools/testing/selftests/drivers/net/lib/py/__init__.py b/tools/testing/selftests/drivers/net/lib/py/__init__.py
index 6efb635c5bfa..0486405c5c70 100644
--- a/tools/testing/selftests/drivers/net/lib/py/__init__.py
+++ b/tools/testing/selftests/drivers/net/lib/py/__init__.py
@@ -51,11 +51,12 @@ try:
     from .load import GenerateTraffic, Iperf3Runner
     from .remote import Remote
     from .ethtool import ethtool_ret, onoff, wait_for_link, wait_for_aneg, \
+        controllable_lp, require_controllable_lp, \
         forced_link_settings
 
     __all__ += ["NetDrvEnv", "NetDrvEpEnv", "NetDrvContEnv", "GenerateTraffic",
                 "Remote", "Iperf3Runner",
-                "ethtool_ret", "onoff", "wait_for_link", "wait_for_aneg",
+                "ethtool_ret", "onoff", "wait_for_link", "wait_for_aneg", "controllable_lp", "require_controllable_lp",
                 "forced_link_settings"]
 except ModuleNotFoundError as e:
     print("Failed importing `net` library from kernel sources")
diff --git a/tools/testing/selftests/drivers/net/lib/py/ethtool.py b/tools/testing/selftests/drivers/net/lib/py/ethtool.py
index 438bf08ccab7..43e197ac12e5 100644
--- a/tools/testing/selftests/drivers/net/lib/py/ethtool.py
+++ b/tools/testing/selftests/drivers/net/lib/py/ethtool.py
@@ -9,7 +9,7 @@ import json
 import os
 import time
 
-from lib.py import cmd, ethtool
+from lib.py import KsftSkipEx, cmd, ethtool, ip, ksft_pr
 
 _strerrors = {os.strerror(e): e for e in errno.errorcode}
 
@@ -117,6 +117,64 @@ def wait_for_link(cfg) -> bool:
     # the remote to report link up, let's wait a bit less
     return wait_for_link_remote(cfg, timeout = 3)
 
+def controllable_lp(cfg) -> bool:
+    """ Whether the remote interface is the link partner of the local one.
+
+    For low level ethtool tests, we need to have the remote directly connected
+    to the local host (i.e. not through a switch).
+
+    This is tested by taking the local link down and checking that the
+    remote's link drops.
+
+    :param cfg: test config
+    :returns: True if the remote is our link partner
+    """
+    known = getattr(cfg, "lp_controllable", None)
+    if known is not None:
+        return known
+
+    # Set both ends up, wait for up
+    ip(f"link set {cfg.ifname} up")
+    ip(f"link set {cfg.remote_ifname} up", host=cfg.remote)
+
+    # No link established
+    if not wait_for_link(cfg):
+        ksft_pr(f"{cfg.remote_ifname} is not the link partner: no link with both ends up")
+        cfg.lp_controllable = False
+        return False
+
+    ip(f"link set {cfg.ifname} down")
+
+    deadline = time.monotonic() + 3
+    dropped = False
+    while time.monotonic() < deadline and not dropped:
+        dropped = not ethtool(f"{cfg.remote_ifname}", json=True,
+                              host=cfg.remote)[0]["link-detected"]
+        time.sleep(0.1)
+
+    ip(f"link set {cfg.ifname} up")
+
+    if not dropped:
+        ksft_pr(f"{cfg.remote_ifname} is not the link partner: "
+                f"it kept its link through {cfg.ifname} going down")
+        cfg.lp_controllable = False
+        wait_for_link(cfg)
+        return False
+
+    cfg.lp_controllable = wait_for_link(cfg)
+    if not cfg.lp_controllable:
+        ksft_pr(f"{cfg.remote_ifname} is not the link partner: "
+                f"no link back after {cfg.ifname} came up")
+    return cfg.lp_controllable
+
+def require_controllable_lp(cfg) -> None:
+    """ Skip if the remote isn't directly connected to the local device
+
+    :param cfg: test config
+    """
+    if not controllable_lp(cfg):
+        raise KsftSkipEx(f"{cfg.remote_ifname} is not directly connected to {cfg.ifname}")
+
 def forced_link_settings(cfg) -> str:
     """ Returns a string to pass to ethtool -s with speed/duplex corresponding
         to the current settings.
-- 
2.55.0


^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH net-next 3/3] selftests: drv-net: pause: pause autoneg tests
  2026-09-20 16:47 [PATCH net-next 0/3] selftests: drv-net: Introduce flow-control selftest Maxime Chevallier (Netdev Foundation)
                   ` (2 preceding siblings ...)
  2026-09-20 16:47 ` [PATCH net-next 2/3] selftests: drv-net: pause: Validate the pause autonegotiation with a partner Maxime Chevallier (Netdev Foundation)
@ 2026-09-20 16:47 ` Maxime Chevallier (Netdev Foundation)
  2026-09-20 16:47 ` [PATCH net-next 3/3] selftests: drv-net: pause: Validate pause autoneg interactions with link autoneg Maxime Chevallier (Netdev Foundation)
  4 siblings, 0 replies; 6+ messages in thread
From: Maxime Chevallier (Netdev Foundation) @ 2026-09-20 16:47 UTC (permalink / raw)
  To: Andrew Lunn, Jakub Kicinski, davem, 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, netdev, linux-kernel, linux-doc

Pause autonegotiation happens with the link partner using the same words
as the link negotiation, used for speed and duplex exchanges. However,
pause and link autonegotiation can be separately toggled :

ethtool -s eth0 autoneg on # Enable link negotiation
ethtool -A eth0 autoneg on # Enable Pause negotiation

Pause can't be negotiated if the link autoneg isn't enabled.

Pause autoneg and link autoneg settings must not interfere with one
another when user is configuring them :
 - If Pause autoneg is on, it must stay on when link autoneg is disabled
   (even though Pause won't actually be negotiated)
 - If Pause autoneg is off, it must stay off when link autoneg is
   enabled

Introduce a set of tests to verify that pause and link autoneg are
behaving correctly, in particular that the user intent is correctly
cached when toggling link autoneg.

Signed-off-by: Maxime Chevallier (Netdev Foundation) <maxime.chevallier@bootlin.com>
---
 .../drivers/net/hw/lib/py/__init__.py         |   2 +-
 .../testing/selftests/drivers/net/hw/pause.py | 284 ++++++++++++++++++
 .../selftests/drivers/net/lib/py/__init__.py  |   4 +-
 .../selftests/drivers/net/lib/py/ethtool.py   |  14 +
 4 files changed, 301 insertions(+), 3 deletions(-)

diff --git a/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py b/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py
index b23f53bcd3b0..958af4beef38 100644
--- a/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py
+++ b/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py
@@ -34,7 +34,7 @@ try:
     from drivers.net.lib.py import GenerateTraffic, Remote, Iperf3Runner
     from drivers.net.lib.py import NetDrvEnv, NetDrvEpEnv, NetDrvContEnv
     from drivers.net.lib.py import ethtool_ret, onoff, wait_for_link, wait_for_aneg, \
-        controllable_lp, require_controllable_lp, \
+        controllable_lp, require_controllable_lp, require_link_autoneg, \
         forced_link_settings
 
     __all__ = ["NetNS", "NetNSEnter", "NetdevSimDev", "UserNetNS",
diff --git a/tools/testing/selftests/drivers/net/hw/pause.py b/tools/testing/selftests/drivers/net/hw/pause.py
index e291c7d8583e..2358d41a7412 100755
--- a/tools/testing/selftests/drivers/net/hw/pause.py
+++ b/tools/testing/selftests/drivers/net/hw/pause.py
@@ -30,6 +30,7 @@ from lib.py import (
     ksft_variants,
     onoff,
     require_controllable_lp,
+    require_link_autoneg,
     wait_for_aneg,
     wait_for_link,
 )
@@ -242,6 +243,23 @@ def get_peer_pause_lp_advertising(cfg) -> tuple[int, list[str]]:
         ksft_pr(f"Warning: {cfg.remote_ifname} does not report the LP's advertising")
         return errno.EOPNOTSUPP, None
 
+def require_pause_supported_anyof(cfg, linkmodes) -> None:
+    """ Checks if the local device supports pause at all, by looking at the
+        supported bitfield. Raises a skip it's not supported.
+
+        :param cfg: test config
+    """
+    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 in pause_support:
+            return
+
+    raise KsftSkipEx(f"Local device doesn't support any of {linkmodes}")
+
 def require_pause_supported_allof(cfg, linkmodes) -> None:
     """ Checks if the local device supports the passed linkmodes
 
@@ -285,6 +303,51 @@ def require_peer_pause_supported_allof(cfg, linkmodes) -> None:
         if lm not in pause_support:
             raise KsftSkipEx(f"Remote device doesn't support {lm}")
 
+def supported_pauseparams(cfg) -> tuple[int, int]:
+    """ The rx/tx params covering every mode the local device supports """
+    _, pause_support = get_local_pause_supported(cfg)
+    if "Pause" in pause_support:
+        return 1, 1
+    if "Asym_Pause" in pause_support:
+        return 0, 1
+
+    raise KsftSkipEx("Local device doesn't support pause")
+
+def set_local_pause_autoneg(cfg, aneg) -> int:
+    """ Enable or disable pause autoneg """
+    ret, _ = ethtool_ret(f"-A {cfg.ifname} autoneg {onoff(aneg)}",
+                         is_get=False)
+    return ret
+
+def check_local_pauseparams(cfg, aneg, rx, tx) -> None:
+    """ Validates that the pauseparams reported from ethtool -a are
+        exactly the 3 passed parameters.
+    """
+    ret, params = get_local_pauseparams(cfg)
+    ksft_eq(ret, 0)
+    if ret != 0:
+        return
+
+    ksft_eq(params["autonegotiate"], bool(aneg), "pause autoneg")
+    ksft_eq(params["rx"], bool(rx), "rx pause")
+    ksft_eq(params["tx"], bool(tx), "tx pause")
+
+def check_local_advertising(cfg, linkmodes) -> None:
+    """ Verify that the local device advertises exactly the pause modes
+        passed as parameters
+    """
+    _, adv = get_local_pause_advertising(cfg)
+    ksft_eq(adv, linkmodes, "advertised pause modes")
+
+def check_local_lp_advertising(cfg, linkmodes) -> None:
+    """ Verify that the local device reports exactly the lp_advertised pause
+        modes passed as parameters
+    """
+
+    ret, adv = get_local_pause_lp_advertising(cfg)
+    if ret == 0:
+        ksft_eq(adv, linkmodes, "link partner advertised pause modes")
+
 def expect_pauseparams_set(ret, linkmodes, supported, note) -> None:
     """ Whether ethtool -A had to work or to be refused, given what the local
         device supports
@@ -674,6 +737,223 @@ def pause_aneg_resolution(cfg, settings) -> None:
         ksft_eq(remote_pauseparams["negotiated"]["rx"], expected_lp_rx)
         ksft_eq(remote_pauseparams["negotiated"]["tx"], expected_lp_tx)
 
+# Pause autoneg enable/disable vs advertised linkmodes
+@ksft_disruptive
+def pause_autoneg_state_adv(cfg) -> None:
+    """Validate that toggling pause advertising changes the advertised linkmodes
+
+    When disabling pause autoneg, we enforce the pause params based on what user
+    asks, instead of relying on the negociation process (which may not be what
+    the user asked for). In forced pause settings, we don't advertise pause and
+    asym_pause bits.
+
+    Failing this test means that .set_pauseparam in the MAC driver doesn't
+    forward to the PHY (in charge of advertising these bits) that we are in
+    fixed pause mode.
+    """
+
+    require_pause_supported_anyof(cfg, ["Pause", "Asym_Pause"])
+    require_controllable_lp(cfg)
+    pause_setup(cfg)
+
+    set_peer_pauseparams(cfg, True, True, True)
+    ksft_eq(wait_for_aneg(cfg), True)
+
+    rx, tx = supported_pauseparams(cfg)
+
+    # Enable all possible pauseparams with pause autoneg
+    ret = set_local_pauseparams(cfg, rx, tx, True)
+    ksft_eq(ret, 0)
+    ksft_eq(wait_for_aneg(cfg), True)
+
+    # Make sure we advertise them
+    ret, adv = get_local_pause_advertising(cfg)
+    ksft_eq(ret, 0)
+    ksft_eq(adv, pause_to_linkmodes(rx, tx))
+
+    # Disable pause autoneg
+    ret = set_local_pauseparams(cfg, rx, tx, False)
+    ksft_eq(ret, 0)
+
+    # This may trigger a link renegociation
+    ksft_eq(wait_for_aneg(cfg), True)
+
+    # We shouldn't be advertising anything anymore
+    ret, adv = get_local_pause_advertising(cfg)
+    ksft_eq(ret, 0)
+    ksft_eq(adv, [])
+
+    # Validate on the LP that we aren't advertising anything
+    ret, adv = get_peer_pause_lp_advertising(cfg)
+    if ret == errno.EOPNOTSUPP:
+        return
+
+    ksft_eq(ret, 0)
+    ksft_eq(adv, [])
+
+# Pause autoneg : Negotiated pause params vs fixed pause params
+@ksft_disruptive
+def pause_autoneg_state_params(cfg) -> None:
+    """Validate the pause params when transitioning between fixed pause
+       params and negotiated ones. The goal is to make sure that user
+       intent on the RX and TX pause params are stored when user decides
+       to use negotiated parameters instead. The main gotcha lies on the
+       fact that when pause autoneg is used, the autoneg result may differ
+       from the user intent.
+
+    Failing this test means the MAC driver is overwriting the user intent
+    when switching to forced pause.
+    """
+
+    require_pause_supported_allof(cfg, ["Pause", "Asym_Pause"])
+    require_controllable_lp(cfg)
+    require_peer_pause_supported_allof(cfg, ["Pause"])
+    require_link_autoneg(cfg)
+    pause_setup(cfg)
+
+    # Set peer user intent to RX on TX on, with Pause autoneg on
+    set_peer_pauseparams(cfg, 1, 1, True)
+    ksft_eq(wait_for_aneg(cfg), True)
+
+    # Set the local intent to RX on TX off with pause autoneg
+    ksft_eq(set_local_pauseparams(cfg, 1, 0, True), 0)
+    ksft_eq(wait_for_aneg(cfg), True)
+
+    check_local_pauseparams(cfg, True, 1, 0)
+    # Peer advertisiong Pause + Asym and us advertising Pause means we are
+    # actually using RX on TX on here, which is different than the intent.
+    check_local_advertising(cfg, ["Pause", "Asym_Pause"])
+    check_local_lp_advertising(cfg, ["Pause"])
+
+    # Disable pause autoneg
+    ksft_eq(set_local_pause_autoneg(cfg, False), 0)
+    ksft_eq(wait_for_aneg(cfg), True)
+
+    # The pauseparams must still be what we configured before, and not the
+    # previously negotiated ones
+    check_local_pauseparams(cfg, False, 1, 0)
+
+    # Re-enable autoneg
+    ksft_eq(set_local_pause_autoneg(cfg, True), 0)
+    ksft_eq(wait_for_aneg(cfg), True)
+
+    check_local_pauseparams(cfg, True, 1, 0)
+    # We must be advertising our intent again, and not RX on TX on, which would
+    # be "Pause" only.
+    check_local_advertising(cfg, ["Pause", "Asym_Pause"])
+    check_local_lp_advertising(cfg, ["Pause"])
+
+# Pause autoneg vs Link autoneg
+
+@ksft_disruptive
+def pause_autoneg_off_while_link_autoneg_on(cfg) -> None:
+    """ Validate that when link autoneg is on but pause autoneg is off, we do
+        not use negotiated pause parameters.
+
+        - Skip if pause not supported.
+
+        - Requirements :
+        - Link partner: link up, link autoneg on, pause autoneg on,
+          pause tx and rx on
+        - Local device starting conditions : link on, link autoneg on,
+          pause autoneg on, pause tx <on if supported> rx <on if supported>
+
+        Failing this test means the MAC driver incorrectly accounts for the
+        negotiated pause parameters even with pause aneg off, likely due to
+        confusion between link autoneg and pause autoneg.
+    """
+    require_pause_supported_anyof(cfg, ["Pause", "Asym_Pause"])
+    require_controllable_lp(cfg)
+    require_peer_pause_supported_allof(cfg, ["Pause"])
+    require_link_autoneg(cfg)
+    pause_setup(cfg)
+
+    rx, tx = supported_pauseparams(cfg)
+
+    # Enable pause autoneg with all the locally supported modes enabled
+    set_peer_pauseparams(cfg, 1, 1, True)
+    ksft_eq(set_local_pauseparams(cfg, rx, tx, True), 0)
+    ksft_eq(wait_for_aneg(cfg), True)
+
+    # Disable Pause autoneg
+    ksft_eq(set_local_pauseparams(cfg, rx, tx, False), 0)
+    ksft_eq(wait_for_aneg(cfg), True)
+    # Pause autoneg must read "disabled"
+    check_local_pauseparams(cfg, False, rx, tx)
+
+    set_peer_pauseparams(cfg, 0, 0, True)
+    ksft_eq(wait_for_aneg(cfg, link_drop=True), True)
+
+    ip(f"link set {cfg.remote_ifname} down", host=cfg.remote)
+    ip(f"link set {cfg.remote_ifname} up", host=cfg.remote)
+    ksft_eq(wait_for_aneg(cfg, link_drop=True), True)
+
+    # Pause autoneg must still be off even after a link renegotiation
+    check_local_pauseparams(cfg, False, rx, tx)
+
+@ksft_disruptive
+def pause_autoneg_link_autoneg(cfg) -> None:
+    """Validate pause autoneg and link autoneg interactions. The link autoneg's
+       admin status (i.e. do we autoneg link parameters or force them) must not
+       impact the pause autoneg status. While link autoneg is disabled, we don't
+       negotiate the pause params, however we must keep pause autoneg on as this
+       is the user intent. When link autoneg is re-enabled, pause params must be
+       derived from the negotiation.
+
+    Both ends are forced at the speed and duplex the link runs at. A link that
+    does not come back forced is a skip, not a verdict: 1000BASE-T resolves
+    master/slave through autoneg and rarely links without it.
+    """
+
+    require_pause_supported_anyof(cfg, ["Pause", "Asym_Pause"])
+    require_controllable_lp(cfg)
+    require_peer_pause_supported_allof(cfg, ["Pause"])
+    require_link_autoneg(cfg)
+    pause_setup(cfg)
+
+    rx, tx = supported_pauseparams(cfg)
+    adv = pause_to_linkmodes(rx, tx)
+
+    # Enable all possible pause modes and autoneg
+    set_peer_pauseparams(cfg, 1, 1, True)
+    ksft_eq(set_local_pauseparams(cfg, rx, tx, True), 0)
+    ksft_eq(wait_for_aneg(cfg), True)
+
+    check_local_pauseparams(cfg, True, rx, tx)
+    check_local_advertising(cfg, adv)
+    check_local_lp_advertising(cfg, ["Pause"])
+
+    # Disable link autoneg, at the speed and duplex the link runs at
+    forced = forced_link_settings(cfg)
+    if not forced:
+        raise KsftSkipEx("Can't tell what to force the link at")
+
+    ret, _ = ethtool_ret(f"-s {cfg.remote_ifname} autoneg off {forced}",
+                         is_get=False, host=cfg.remote)
+    if ret != 0:
+        raise KsftSkipEx(f"Can't force the peer's link at {forced}")
+
+    ret, _ = ethtool_ret(f"-s {cfg.ifname} autoneg off {forced}",
+                         is_get=False)
+    if ret != 0:
+        raise KsftSkipEx(f"Can't force the link at {forced}")
+
+    if not wait_for_aneg(cfg):
+        raise KsftSkipEx(f"No link when forced at {forced}")
+
+    # We must have pause autoneg still enabled, even if we don't negotiate pause
+    check_local_pauseparams(cfg, True, rx, tx)
+
+    # Re-enable autoneg
+    ethtool(f"-s {cfg.remote_ifname} autoneg on", host=cfg.remote)
+    ethtool(f"-s {cfg.ifname} autoneg on")
+    ksft_eq(wait_for_aneg(cfg), True)
+
+    # Pause autoneg must still be on
+    check_local_pauseparams(cfg, True, rx, tx)
+    check_local_advertising(cfg, adv)
+    check_local_lp_advertising(cfg, ["Pause"])
+
 def main() -> None:
     """ The hardware pause tests, on the interface the env names """
     with NetDrvEpEnv(__file__, nsim_test=False) as cfg:
@@ -681,6 +961,10 @@ def main() -> None:
         ksft_run([pause_test_support,
                   pause_advertising_test,
                   pause_aneg_resolution,
+                  pause_autoneg_state_adv,
+                  pause_autoneg_state_params,
+                  pause_autoneg_off_while_link_autoneg_on,
+                  pause_autoneg_link_autoneg,
                   ],
                  args=(cfg, ))
     ksft_exit()
diff --git a/tools/testing/selftests/drivers/net/lib/py/__init__.py b/tools/testing/selftests/drivers/net/lib/py/__init__.py
index 0486405c5c70..ad0ba71b725a 100644
--- a/tools/testing/selftests/drivers/net/lib/py/__init__.py
+++ b/tools/testing/selftests/drivers/net/lib/py/__init__.py
@@ -51,13 +51,13 @@ try:
     from .load import GenerateTraffic, Iperf3Runner
     from .remote import Remote
     from .ethtool import ethtool_ret, onoff, wait_for_link, wait_for_aneg, \
-        controllable_lp, require_controllable_lp, \
+        controllable_lp, require_controllable_lp, require_link_autoneg, \
         forced_link_settings
 
     __all__ += ["NetDrvEnv", "NetDrvEpEnv", "NetDrvContEnv", "GenerateTraffic",
                 "Remote", "Iperf3Runner",
                 "ethtool_ret", "onoff", "wait_for_link", "wait_for_aneg", "controllable_lp", "require_controllable_lp",
-                "forced_link_settings"]
+                "require_link_autoneg", "forced_link_settings"]
 except ModuleNotFoundError as e:
     print("Failed importing `net` library from kernel sources")
     print(str(e))
diff --git a/tools/testing/selftests/drivers/net/lib/py/ethtool.py b/tools/testing/selftests/drivers/net/lib/py/ethtool.py
index 43e197ac12e5..00d6805ee942 100644
--- a/tools/testing/selftests/drivers/net/lib/py/ethtool.py
+++ b/tools/testing/selftests/drivers/net/lib/py/ethtool.py
@@ -175,6 +175,20 @@ def require_controllable_lp(cfg) -> None:
     if not controllable_lp(cfg):
         raise KsftSkipEx(f"{cfg.remote_ifname} is not directly connected to {cfg.ifname}")
 
+def require_link_autoneg(cfg) -> None:
+    """ Skip if local or remote don't support link autoneg
+
+    :param cfg: test config
+    """
+    # Does local device support link aneg
+    if not ethtool(f"{cfg.ifname}", json=True)[0]["supports-auto-negotiation"]:
+        raise KsftSkipEx(f"{cfg.ifname} doesn't support link autoneg")
+
+    # Does remote device support link aneg
+    if not ethtool(f"{cfg.remote_ifname}",
+                   json=True, host=cfg.remote)[0]["supports-auto-negotiation"]:
+        raise KsftSkipEx(f"Remote {cfg.remote_ifname} doesn't support link autoneg")
+
 def forced_link_settings(cfg) -> str:
     """ Returns a string to pass to ethtool -s with speed/duplex corresponding
         to the current settings.
-- 
2.55.0


^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH net-next 3/3] selftests: drv-net: pause: Validate pause autoneg interactions with link autoneg
  2026-09-20 16:47 [PATCH net-next 0/3] selftests: drv-net: Introduce flow-control selftest Maxime Chevallier (Netdev Foundation)
                   ` (3 preceding siblings ...)
  2026-09-20 16:47 ` [PATCH net-next 3/3] selftests: drv-net: pause: pause autoneg tests Maxime Chevallier (Netdev Foundation)
@ 2026-09-20 16:47 ` Maxime Chevallier (Netdev Foundation)
  4 siblings, 0 replies; 6+ messages in thread
From: Maxime Chevallier (Netdev Foundation) @ 2026-09-20 16:47 UTC (permalink / raw)
  To: Andrew Lunn, Jakub Kicinski, davem, 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, netdev, linux-kernel, linux-doc

Pause autonegotiation happens with the link partner using the same words
as the link negotiation, used for speed and duplex exchanges. However,
pause and link autonegotiation can be separately toggled :

ethtool -s eth0 autoneg on # Enable link negotiation
ethtool -A eth0 autoneg on # Enable Pause negotiation

Pause can't be negotiated if the link autoneg isn't enabled.

Pause autoneg and link autoneg settings must not interfere with one
another when user is configuring them :
 - If Pause autoneg is on, it must stay on when link autoneg is disabled
   (even though Pause won't actually be negotiated)
 - If Pause autoneg is off, it must stay off when link autoneg is
   enabled

Introduce a set of tests to verify that pause and link autoneg are
behaving correctly, in particular that the user intent is correctly
cached when toggling link autoneg.

Signed-off-by: Maxime Chevallier (Netdev Foundation) <maxime.chevallier@bootlin.com>
---
 .../drivers/net/hw/lib/py/__init__.py         |   2 +-
 .../testing/selftests/drivers/net/hw/pause.py | 284 ++++++++++++++++++
 .../selftests/drivers/net/lib/py/__init__.py  |   4 +-
 .../selftests/drivers/net/lib/py/ethtool.py   |  14 +
 4 files changed, 301 insertions(+), 3 deletions(-)

diff --git a/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py b/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py
index b23f53bcd3b0..958af4beef38 100644
--- a/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py
+++ b/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py
@@ -34,7 +34,7 @@ try:
     from drivers.net.lib.py import GenerateTraffic, Remote, Iperf3Runner
     from drivers.net.lib.py import NetDrvEnv, NetDrvEpEnv, NetDrvContEnv
     from drivers.net.lib.py import ethtool_ret, onoff, wait_for_link, wait_for_aneg, \
-        controllable_lp, require_controllable_lp, \
+        controllable_lp, require_controllable_lp, require_link_autoneg, \
         forced_link_settings
 
     __all__ = ["NetNS", "NetNSEnter", "NetdevSimDev", "UserNetNS",
diff --git a/tools/testing/selftests/drivers/net/hw/pause.py b/tools/testing/selftests/drivers/net/hw/pause.py
index e291c7d8583e..2358d41a7412 100755
--- a/tools/testing/selftests/drivers/net/hw/pause.py
+++ b/tools/testing/selftests/drivers/net/hw/pause.py
@@ -30,6 +30,7 @@ from lib.py import (
     ksft_variants,
     onoff,
     require_controllable_lp,
+    require_link_autoneg,
     wait_for_aneg,
     wait_for_link,
 )
@@ -242,6 +243,23 @@ def get_peer_pause_lp_advertising(cfg) -> tuple[int, list[str]]:
         ksft_pr(f"Warning: {cfg.remote_ifname} does not report the LP's advertising")
         return errno.EOPNOTSUPP, None
 
+def require_pause_supported_anyof(cfg, linkmodes) -> None:
+    """ Checks if the local device supports pause at all, by looking at the
+        supported bitfield. Raises a skip it's not supported.
+
+        :param cfg: test config
+    """
+    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 in pause_support:
+            return
+
+    raise KsftSkipEx(f"Local device doesn't support any of {linkmodes}")
+
 def require_pause_supported_allof(cfg, linkmodes) -> None:
     """ Checks if the local device supports the passed linkmodes
 
@@ -285,6 +303,51 @@ def require_peer_pause_supported_allof(cfg, linkmodes) -> None:
         if lm not in pause_support:
             raise KsftSkipEx(f"Remote device doesn't support {lm}")
 
+def supported_pauseparams(cfg) -> tuple[int, int]:
+    """ The rx/tx params covering every mode the local device supports """
+    _, pause_support = get_local_pause_supported(cfg)
+    if "Pause" in pause_support:
+        return 1, 1
+    if "Asym_Pause" in pause_support:
+        return 0, 1
+
+    raise KsftSkipEx("Local device doesn't support pause")
+
+def set_local_pause_autoneg(cfg, aneg) -> int:
+    """ Enable or disable pause autoneg """
+    ret, _ = ethtool_ret(f"-A {cfg.ifname} autoneg {onoff(aneg)}",
+                         is_get=False)
+    return ret
+
+def check_local_pauseparams(cfg, aneg, rx, tx) -> None:
+    """ Validates that the pauseparams reported from ethtool -a are
+        exactly the 3 passed parameters.
+    """
+    ret, params = get_local_pauseparams(cfg)
+    ksft_eq(ret, 0)
+    if ret != 0:
+        return
+
+    ksft_eq(params["autonegotiate"], bool(aneg), "pause autoneg")
+    ksft_eq(params["rx"], bool(rx), "rx pause")
+    ksft_eq(params["tx"], bool(tx), "tx pause")
+
+def check_local_advertising(cfg, linkmodes) -> None:
+    """ Verify that the local device advertises exactly the pause modes
+        passed as parameters
+    """
+    _, adv = get_local_pause_advertising(cfg)
+    ksft_eq(adv, linkmodes, "advertised pause modes")
+
+def check_local_lp_advertising(cfg, linkmodes) -> None:
+    """ Verify that the local device reports exactly the lp_advertised pause
+        modes passed as parameters
+    """
+
+    ret, adv = get_local_pause_lp_advertising(cfg)
+    if ret == 0:
+        ksft_eq(adv, linkmodes, "link partner advertised pause modes")
+
 def expect_pauseparams_set(ret, linkmodes, supported, note) -> None:
     """ Whether ethtool -A had to work or to be refused, given what the local
         device supports
@@ -674,6 +737,223 @@ def pause_aneg_resolution(cfg, settings) -> None:
         ksft_eq(remote_pauseparams["negotiated"]["rx"], expected_lp_rx)
         ksft_eq(remote_pauseparams["negotiated"]["tx"], expected_lp_tx)
 
+# Pause autoneg enable/disable vs advertised linkmodes
+@ksft_disruptive
+def pause_autoneg_state_adv(cfg) -> None:
+    """Validate that toggling pause advertising changes the advertised linkmodes
+
+    When disabling pause autoneg, we enforce the pause params based on what user
+    asks, instead of relying on the negociation process (which may not be what
+    the user asked for). In forced pause settings, we don't advertise pause and
+    asym_pause bits.
+
+    Failing this test means that .set_pauseparam in the MAC driver doesn't
+    forward to the PHY (in charge of advertising these bits) that we are in
+    fixed pause mode.
+    """
+
+    require_pause_supported_anyof(cfg, ["Pause", "Asym_Pause"])
+    require_controllable_lp(cfg)
+    pause_setup(cfg)
+
+    set_peer_pauseparams(cfg, True, True, True)
+    ksft_eq(wait_for_aneg(cfg), True)
+
+    rx, tx = supported_pauseparams(cfg)
+
+    # Enable all possible pauseparams with pause autoneg
+    ret = set_local_pauseparams(cfg, rx, tx, True)
+    ksft_eq(ret, 0)
+    ksft_eq(wait_for_aneg(cfg), True)
+
+    # Make sure we advertise them
+    ret, adv = get_local_pause_advertising(cfg)
+    ksft_eq(ret, 0)
+    ksft_eq(adv, pause_to_linkmodes(rx, tx))
+
+    # Disable pause autoneg
+    ret = set_local_pauseparams(cfg, rx, tx, False)
+    ksft_eq(ret, 0)
+
+    # This may trigger a link renegociation
+    ksft_eq(wait_for_aneg(cfg), True)
+
+    # We shouldn't be advertising anything anymore
+    ret, adv = get_local_pause_advertising(cfg)
+    ksft_eq(ret, 0)
+    ksft_eq(adv, [])
+
+    # Validate on the LP that we aren't advertising anything
+    ret, adv = get_peer_pause_lp_advertising(cfg)
+    if ret == errno.EOPNOTSUPP:
+        return
+
+    ksft_eq(ret, 0)
+    ksft_eq(adv, [])
+
+# Pause autoneg : Negotiated pause params vs fixed pause params
+@ksft_disruptive
+def pause_autoneg_state_params(cfg) -> None:
+    """Validate the pause params when transitioning between fixed pause
+       params and negotiated ones. The goal is to make sure that user
+       intent on the RX and TX pause params are stored when user decides
+       to use negotiated parameters instead. The main gotcha lies on the
+       fact that when pause autoneg is used, the autoneg result may differ
+       from the user intent.
+
+    Failing this test means the MAC driver is overwriting the user intent
+    when switching to forced pause.
+    """
+
+    require_pause_supported_allof(cfg, ["Pause", "Asym_Pause"])
+    require_controllable_lp(cfg)
+    require_peer_pause_supported_allof(cfg, ["Pause"])
+    require_link_autoneg(cfg)
+    pause_setup(cfg)
+
+    # Set peer user intent to RX on TX on, with Pause autoneg on
+    set_peer_pauseparams(cfg, 1, 1, True)
+    ksft_eq(wait_for_aneg(cfg), True)
+
+    # Set the local intent to RX on TX off with pause autoneg
+    ksft_eq(set_local_pauseparams(cfg, 1, 0, True), 0)
+    ksft_eq(wait_for_aneg(cfg), True)
+
+    check_local_pauseparams(cfg, True, 1, 0)
+    # Peer advertisiong Pause + Asym and us advertising Pause means we are
+    # actually using RX on TX on here, which is different than the intent.
+    check_local_advertising(cfg, ["Pause", "Asym_Pause"])
+    check_local_lp_advertising(cfg, ["Pause"])
+
+    # Disable pause autoneg
+    ksft_eq(set_local_pause_autoneg(cfg, False), 0)
+    ksft_eq(wait_for_aneg(cfg), True)
+
+    # The pauseparams must still be what we configured before, and not the
+    # previously negotiated ones
+    check_local_pauseparams(cfg, False, 1, 0)
+
+    # Re-enable autoneg
+    ksft_eq(set_local_pause_autoneg(cfg, True), 0)
+    ksft_eq(wait_for_aneg(cfg), True)
+
+    check_local_pauseparams(cfg, True, 1, 0)
+    # We must be advertising our intent again, and not RX on TX on, which would
+    # be "Pause" only.
+    check_local_advertising(cfg, ["Pause", "Asym_Pause"])
+    check_local_lp_advertising(cfg, ["Pause"])
+
+# Pause autoneg vs Link autoneg
+
+@ksft_disruptive
+def pause_autoneg_off_while_link_autoneg_on(cfg) -> None:
+    """ Validate that when link autoneg is on but pause autoneg is off, we do
+        not use negotiated pause parameters.
+
+        - Skip if pause not supported.
+
+        - Requirements :
+        - Link partner: link up, link autoneg on, pause autoneg on,
+          pause tx and rx on
+        - Local device starting conditions : link on, link autoneg on,
+          pause autoneg on, pause tx <on if supported> rx <on if supported>
+
+        Failing this test means the MAC driver incorrectly accounts for the
+        negotiated pause parameters even with pause aneg off, likely due to
+        confusion between link autoneg and pause autoneg.
+    """
+    require_pause_supported_anyof(cfg, ["Pause", "Asym_Pause"])
+    require_controllable_lp(cfg)
+    require_peer_pause_supported_allof(cfg, ["Pause"])
+    require_link_autoneg(cfg)
+    pause_setup(cfg)
+
+    rx, tx = supported_pauseparams(cfg)
+
+    # Enable pause autoneg with all the locally supported modes enabled
+    set_peer_pauseparams(cfg, 1, 1, True)
+    ksft_eq(set_local_pauseparams(cfg, rx, tx, True), 0)
+    ksft_eq(wait_for_aneg(cfg), True)
+
+    # Disable Pause autoneg
+    ksft_eq(set_local_pauseparams(cfg, rx, tx, False), 0)
+    ksft_eq(wait_for_aneg(cfg), True)
+    # Pause autoneg must read "disabled"
+    check_local_pauseparams(cfg, False, rx, tx)
+
+    set_peer_pauseparams(cfg, 0, 0, True)
+    ksft_eq(wait_for_aneg(cfg, link_drop=True), True)
+
+    ip(f"link set {cfg.remote_ifname} down", host=cfg.remote)
+    ip(f"link set {cfg.remote_ifname} up", host=cfg.remote)
+    ksft_eq(wait_for_aneg(cfg, link_drop=True), True)
+
+    # Pause autoneg must still be off even after a link renegotiation
+    check_local_pauseparams(cfg, False, rx, tx)
+
+@ksft_disruptive
+def pause_autoneg_link_autoneg(cfg) -> None:
+    """Validate pause autoneg and link autoneg interactions. The link autoneg's
+       admin status (i.e. do we autoneg link parameters or force them) must not
+       impact the pause autoneg status. While link autoneg is disabled, we don't
+       negotiate the pause params, however we must keep pause autoneg on as this
+       is the user intent. When link autoneg is re-enabled, pause params must be
+       derived from the negotiation.
+
+    Both ends are forced at the speed and duplex the link runs at. A link that
+    does not come back forced is a skip, not a verdict: 1000BASE-T resolves
+    master/slave through autoneg and rarely links without it.
+    """
+
+    require_pause_supported_anyof(cfg, ["Pause", "Asym_Pause"])
+    require_controllable_lp(cfg)
+    require_peer_pause_supported_allof(cfg, ["Pause"])
+    require_link_autoneg(cfg)
+    pause_setup(cfg)
+
+    rx, tx = supported_pauseparams(cfg)
+    adv = pause_to_linkmodes(rx, tx)
+
+    # Enable all possible pause modes and autoneg
+    set_peer_pauseparams(cfg, 1, 1, True)
+    ksft_eq(set_local_pauseparams(cfg, rx, tx, True), 0)
+    ksft_eq(wait_for_aneg(cfg), True)
+
+    check_local_pauseparams(cfg, True, rx, tx)
+    check_local_advertising(cfg, adv)
+    check_local_lp_advertising(cfg, ["Pause"])
+
+    # Disable link autoneg, at the speed and duplex the link runs at
+    forced = forced_link_settings(cfg)
+    if not forced:
+        raise KsftSkipEx("Can't tell what to force the link at")
+
+    ret, _ = ethtool_ret(f"-s {cfg.remote_ifname} autoneg off {forced}",
+                         is_get=False, host=cfg.remote)
+    if ret != 0:
+        raise KsftSkipEx(f"Can't force the peer's link at {forced}")
+
+    ret, _ = ethtool_ret(f"-s {cfg.ifname} autoneg off {forced}",
+                         is_get=False)
+    if ret != 0:
+        raise KsftSkipEx(f"Can't force the link at {forced}")
+
+    if not wait_for_aneg(cfg):
+        raise KsftSkipEx(f"No link when forced at {forced}")
+
+    # We must have pause autoneg still enabled, even if we don't negotiate pause
+    check_local_pauseparams(cfg, True, rx, tx)
+
+    # Re-enable autoneg
+    ethtool(f"-s {cfg.remote_ifname} autoneg on", host=cfg.remote)
+    ethtool(f"-s {cfg.ifname} autoneg on")
+    ksft_eq(wait_for_aneg(cfg), True)
+
+    # Pause autoneg must still be on
+    check_local_pauseparams(cfg, True, rx, tx)
+    check_local_advertising(cfg, adv)
+    check_local_lp_advertising(cfg, ["Pause"])
+
 def main() -> None:
     """ The hardware pause tests, on the interface the env names """
     with NetDrvEpEnv(__file__, nsim_test=False) as cfg:
@@ -681,6 +961,10 @@ def main() -> None:
         ksft_run([pause_test_support,
                   pause_advertising_test,
                   pause_aneg_resolution,
+                  pause_autoneg_state_adv,
+                  pause_autoneg_state_params,
+                  pause_autoneg_off_while_link_autoneg_on,
+                  pause_autoneg_link_autoneg,
                   ],
                  args=(cfg, ))
     ksft_exit()
diff --git a/tools/testing/selftests/drivers/net/lib/py/__init__.py b/tools/testing/selftests/drivers/net/lib/py/__init__.py
index 0486405c5c70..ad0ba71b725a 100644
--- a/tools/testing/selftests/drivers/net/lib/py/__init__.py
+++ b/tools/testing/selftests/drivers/net/lib/py/__init__.py
@@ -51,13 +51,13 @@ try:
     from .load import GenerateTraffic, Iperf3Runner
     from .remote import Remote
     from .ethtool import ethtool_ret, onoff, wait_for_link, wait_for_aneg, \
-        controllable_lp, require_controllable_lp, \
+        controllable_lp, require_controllable_lp, require_link_autoneg, \
         forced_link_settings
 
     __all__ += ["NetDrvEnv", "NetDrvEpEnv", "NetDrvContEnv", "GenerateTraffic",
                 "Remote", "Iperf3Runner",
                 "ethtool_ret", "onoff", "wait_for_link", "wait_for_aneg", "controllable_lp", "require_controllable_lp",
-                "forced_link_settings"]
+                "require_link_autoneg", "forced_link_settings"]
 except ModuleNotFoundError as e:
     print("Failed importing `net` library from kernel sources")
     print(str(e))
diff --git a/tools/testing/selftests/drivers/net/lib/py/ethtool.py b/tools/testing/selftests/drivers/net/lib/py/ethtool.py
index 43e197ac12e5..00d6805ee942 100644
--- a/tools/testing/selftests/drivers/net/lib/py/ethtool.py
+++ b/tools/testing/selftests/drivers/net/lib/py/ethtool.py
@@ -175,6 +175,20 @@ def require_controllable_lp(cfg) -> None:
     if not controllable_lp(cfg):
         raise KsftSkipEx(f"{cfg.remote_ifname} is not directly connected to {cfg.ifname}")
 
+def require_link_autoneg(cfg) -> None:
+    """ Skip if local or remote don't support link autoneg
+
+    :param cfg: test config
+    """
+    # Does local device support link aneg
+    if not ethtool(f"{cfg.ifname}", json=True)[0]["supports-auto-negotiation"]:
+        raise KsftSkipEx(f"{cfg.ifname} doesn't support link autoneg")
+
+    # Does remote device support link aneg
+    if not ethtool(f"{cfg.remote_ifname}",
+                   json=True, host=cfg.remote)[0]["supports-auto-negotiation"]:
+        raise KsftSkipEx(f"Remote {cfg.remote_ifname} doesn't support link autoneg")
+
 def forced_link_settings(cfg) -> str:
     """ Returns a string to pass to ethtool -s with speed/duplex corresponding
         to the current settings.
-- 
2.55.0


^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2026-09-20 16:47 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-20 16:47 [PATCH net-next 0/3] selftests: drv-net: Introduce flow-control selftest Maxime Chevallier (Netdev Foundation)
2026-09-20 16:47 ` [PATCH net-next 1/3] selftests: drv-net: Introduce a selftest for ethtool flow control Maxime Chevallier (Netdev Foundation)
2026-09-20 16:47 ` [PATCH net-next 2/3] selftests: drv-net: pause: tests against a controllable link partner Maxime Chevallier (Netdev Foundation)
2026-09-20 16:47 ` [PATCH net-next 2/3] selftests: drv-net: pause: Validate the pause autonegotiation with a partner Maxime Chevallier (Netdev Foundation)
2026-09-20 16:47 ` [PATCH net-next 3/3] selftests: drv-net: pause: pause autoneg tests Maxime Chevallier (Netdev Foundation)
2026-09-20 16:47 ` [PATCH net-next 3/3] selftests: drv-net: pause: Validate pause autoneg interactions with link autoneg Maxime Chevallier (Netdev Foundation)

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®