Netdev List
 help / color / mirror / Atom feed
From: Eric Dumazet <edumazet@google.com>
To: "David S . Miller" <davem@davemloft.net>,
	Jakub Kicinski <kuba@kernel.org>,
	 Paolo Abeni <pabeni@redhat.com>
Cc: Willem de Bruijn <willemb@google.com>,
	Simon Horman <horms@kernel.org>,
	netdev@vger.kernel.org,  eric.dumazet@gmail.com,
	Eric Dumazet <edumazet@google.com>
Subject: [PATCH net-next 5/5] selftests: drivers: net: check the RSS key a device uses
Date: Mon, 21 Sep 2026 18:37:58 +0000	[thread overview]
Message-ID: <20260921183758.1812310-6-edumazet@google.com> (raw)
In-Reply-To: <20260921183758.1812310-1-edumazet@google.com>

The host key is not the whole story: a driver may bring its own key, and
firmware sometimes installs one, in which case nothing the core does helps.

Read back the key and the indirection table the device reports and check
that flows differing only in the low order bits of one hashed field fill
that table, rather than a fraction of it. Both an algebraic check on the
key and a brute force pass hashing the 2 ** q inputs and placing them in
the table.

The fields checked come from the flow hash configuration of the device, so
a NIC hashing 2-tuples is described correctly, and flow types hashing
something we can not place are reported and skipped.

Signed-off-by: Eric Dumazet <edumazet@google.com>
---
 .../testing/selftests/drivers/net/hw/Makefile |   1 +
 .../drivers/net/hw/lib/py/__init__.py         |   5 +
 .../selftests/drivers/net/hw/rss_key.py       | 172 ++++++++++++++++++
 3 files changed, 178 insertions(+)
 create mode 100755 tools/testing/selftests/drivers/net/hw/rss_key.py

diff --git a/tools/testing/selftests/drivers/net/hw/Makefile b/tools/testing/selftests/drivers/net/hw/Makefile
index 8aebdc6feb177c897ccd0f3ed7ea7a5862a6c054..9bcf3c1e8caa24e715e15af4bfecaeacdd522a7a 100644
--- a/tools/testing/selftests/drivers/net/hw/Makefile
+++ b/tools/testing/selftests/drivers/net/hw/Makefile
@@ -46,6 +46,7 @@ TEST_PROGS = \
 	rss_drv.py \
 	rss_flow_label.py \
 	rss_input_xfrm.py \
+	rss_key.py \
 	toeplitz.py \
 	tso.py \
 	userns_devmem.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 81e1d1865cd50cd210bbfda5e702bff1a5cb71bb..96edd9dfe9e3b6ec903bb19c885298a99f081db8 100644
--- a/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py
+++ b/tools/testing/selftests/drivers/net/hw/lib/py/__init__.py
@@ -26,6 +26,8 @@ try:
         fd_read_timeout, ip, rand_port, rand_ports, wait_port_listen, \
         wait_file, ctl_file_write, tool
     from net.lib.py import bpf_map_set, bpf_map_dump, bpf_prog_map_ids
+    from net.lib.py import RSS_KEY_QMAX, rss_key_bit, rss_key_assign_bit, \
+        rss_key_window, rss_key_toeplitz, rss_key_full_rank, rss_key_layout
     from net.lib.py import KsftSkipEx, KsftFailEx, KsftXfailEx
     from net.lib.py import ksft_disruptive, ksft_exit, ksft_pr, ksft_run, \
         ksft_setup, ksft_variants, KsftNamedVariant
@@ -42,6 +44,9 @@ try:
                "fd_read_timeout", "ip", "rand_port", "rand_ports",
                "wait_port_listen", "wait_file", "ctl_file_write", "tool",
                "bpf_map_set", "bpf_map_dump", "bpf_prog_map_ids",
+               "RSS_KEY_QMAX", "rss_key_bit", "rss_key_assign_bit",
+               "rss_key_window", "rss_key_toeplitz", "rss_key_full_rank",
+               "rss_key_layout",
                "KsftSkipEx", "KsftFailEx", "KsftXfailEx",
                "ksft_disruptive", "ksft_exit", "ksft_pr", "ksft_run",
                "ksft_setup", "ksft_variants", "KsftNamedVariant",
diff --git a/tools/testing/selftests/drivers/net/hw/rss_key.py b/tools/testing/selftests/drivers/net/hw/rss_key.py
new file mode 100755
index 0000000000000000000000000000000000000000..dc7095f6663cd31f70118c48ad32f7a706af5ed9
--- /dev/null
+++ b/tools/testing/selftests/drivers/net/hw/rss_key.py
@@ -0,0 +1,172 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+
+"""
+Check that the RSS key a device actually uses spreads flows over all of the
+entries of its indirection table.
+
+Most drivers take their key from netdev_rss_key_fill(), which generates keys
+having that property, but some bring their own and firmware sometimes
+installs one of its own. This reads back the key the device reports, so it
+covers wherever the key came from. See net/lib/py/rsskey.py for what the
+property is and why a random key is not good enough.
+"""
+
+import random
+
+from lib.py import ksft_run, ksft_exit, ksft_pr
+from lib.py import ksft_eq
+from lib.py import KsftSkipEx
+from lib.py import NetDrvEnv, EthtoolFamily, cmd
+from lib.py import RSS_KEY_QMAX, rss_key_assign_bit, rss_key_toeplitz, \
+    rss_key_full_rank, rss_key_layout
+
+# "define" for the ID of the Toeplitz hash function
+ETH_RSS_HASH_TOP = 1
+
+FLOW_TYPES = ("tcp4", "udp4", "tcp6", "udp6")
+
+# How ethtool -n spells the fields of the hash input.
+FIELD_NAMES = {
+    "IP SA": "s",
+    "IP DA": "d",
+    "L3 proto": "t",
+    "L4 bytes 0 & 1 [TCP/UDP src port]": "f",
+    "L4 bytes 2 & 3 [TCP/UDP dst port]": "n",
+    "IPv6 Flow Label": "l",
+}
+
+
+def _get_rss(cfg):
+    """The key and indirection table of @cfg's device, or a skip."""
+    rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
+
+    hkey = rss.get("hkey")
+    if not hkey or not any(hkey):
+        raise KsftSkipEx(f"{cfg.ifname} does not report an RSS key")
+
+    if rss.get("hfunc") != ETH_RSS_HASH_TOP:
+        raise KsftSkipEx(f"{cfg.ifname} does not use the Toeplitz hash")
+
+    if rss.get("input-xfrm"):
+        raise KsftSkipEx(f"{cfg.ifname} transforms the hash input")
+
+    indir = rss.get("indir")
+    if not indir:
+        raise KsftSkipEx(f"{cfg.ifname} does not report an indirection table")
+
+    if len(indir) & (len(indir) - 1):
+        raise KsftSkipEx(f"{cfg.ifname} has {len(indir)} indirection table "
+                         "entries, which is not a power of two")
+
+    return bytes(hkey), indir
+
+
+def _get_layouts(cfg):
+    """The hash input layouts in use, mapped to the flow types sharing them."""
+    layouts = {}
+
+    for fl_type in FLOW_TYPES:
+        proc = cmd(f"ethtool -n {cfg.ifname} rx-flow-hash {fl_type}",
+                   fail=False)
+        if proc.ret:
+            continue
+
+        fields = ""
+        for line in proc.stdout.split("\n")[1:-2]:
+            # if this raises we probably need to add more keys to FIELD_NAMES
+            fields += FIELD_NAMES[line]
+
+        layout, nbits = rss_key_layout(fields, fl_type.endswith("6"))
+        if layout is None:
+            ksft_pr(f"{fl_type}: not checked, hashes fields we can not place "
+                    f"({fields})")
+            continue
+
+        layouts.setdefault((tuple(layout), nbits), []).append(fl_type)
+
+    if not layouts:
+        raise KsftSkipEx("no flow type with a hash input we can describe")
+
+    return layouts
+
+
+def test_rss_key_rank(cfg) -> None:
+    """The key has to be non singular for the size of the table."""
+    hkey, indir = _get_rss(cfg)
+    q = min((len(indir) - 1).bit_length(), RSS_KEY_QMAX)
+    degenerate = []
+
+    if not q:
+        raise KsftSkipEx("the indirection table has a single entry")
+
+    for (layout, _), fl_types in _get_layouts(cfg).items():
+        for name, lsb in layout:
+            if lsb + 32 > len(hkey) * 8:
+                ksft_pr(f"{name}: not checked, the key is {len(hkey)} bytes")
+                continue
+
+            if not rss_key_full_rank(hkey, lsb, q):
+                degenerate.append(f"{'/'.join(fl_types)} {name}")
+
+    for bad in degenerate:
+        ksft_pr(f"degenerate: {bad}")
+
+    ksft_eq(len(degenerate), 0,
+            f"the key of {cfg.ifname} does not spread flows over the "
+            f"{1 << q} entries of its indirection table")
+
+
+def test_rss_key_spread(cfg) -> None:
+    """Hash bursts differing in one field only, and place them in the table."""
+    hkey, indir = _get_rss(cfg)
+    q = (len(indir) - 1).bit_length()
+    collisions = []
+
+    if q > RSS_KEY_QMAX:
+        raise KsftSkipEx(f"{len(indir)} indirection table entries is more "
+                         "than the kernel guarantees")
+    if not q:
+        raise KsftSkipEx("the indirection table has a single entry")
+
+    for (layout, nbits), fl_types in _get_layouts(cfg).items():
+        # Unlike the rank check, this hashes the whole input, so it reads
+        # the key up to 31 bits past its last bit rather than past the last
+        # bit of one field.
+        if nbits + 31 > len(hkey) * 8:
+            ksft_pr(f"{'/'.join(fl_types)}: not checked, the key is "
+                    f"{len(hkey)} bytes")
+            continue
+
+        for name, lsb in layout:
+            inp = bytearray(random.randbytes(nbits // 8))
+            entries = set()
+            for value in range(1 << q):
+                for bit in range(q):
+                    rss_key_assign_bit(inp, lsb - bit, value & (1 << bit))
+                hash_ = rss_key_toeplitz(hkey, inp, nbits)
+                entries.add(hash_ & (len(indir) - 1))
+
+            if len(entries) != 1 << q:
+                collisions.append(f"{'/'.join(fl_types)} {name} reaches "
+                                  f"{len(entries)} of the {1 << q} entries")
+
+    for bad in collisions:
+        ksft_pr(bad)
+
+    ksft_eq(len(collisions), 0,
+            f"flows differing in one field only do not fill the "
+            f"indirection table of {cfg.ifname}")
+
+
+def main() -> None:
+    """ Ksft boiler plate main """
+
+    with NetDrvEnv(__file__, nsim_test=False) as cfg:
+        cfg.ethnl = EthtoolFamily()
+        ksft_run(globs=globals(), case_pfx={"test_"}, args=(cfg, ))
+    ksft_exit()
+
+
+if __name__ == "__main__":
+    main()
-- 
2.55.0.1082.g2b9226bbc0-goog


  parent reply	other threads:[~2026-09-21 18:38 UTC|newest]

Thread overview: 15+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-21 18:37 [PATCH net-next 0/5] net: ethtool: make netdev_rss_key_fill() spread flows over all queues Eric Dumazet
2026-09-21 18:37 ` [PATCH net-next 1/5] net: synchronize proc_do_rss_key() with netdev_rss_key_fill() Eric Dumazet
2026-09-21 19:51   ` Jakub Kicinski
2026-09-21 19:53     ` Jakub Kicinski
2026-09-21 20:03     ` Eric Dumazet
2026-09-21 18:37 ` [PATCH net-next 2/5] net: ethtool: generate RSS keys that spread flows over all queues Eric Dumazet
2026-09-21 19:59   ` Jakub Kicinski
2026-09-21 18:37 ` [PATCH net-next 3/5] net: ethtool: add KUnit tests for the generated RSS key Eric Dumazet
2026-09-21 20:03   ` Jakub Kicinski
2026-09-21 20:10     ` Eric Dumazet
2026-09-21 20:35       ` Eric Dumazet
2026-09-21 18:37 ` [PATCH net-next 4/5] selftests: net: check the quality of the host " Eric Dumazet
2026-09-21 18:37 ` Eric Dumazet [this message]
2026-09-21 20:09   ` [PATCH net-next 5/5] selftests: drivers: net: check the RSS key a device uses Jakub Kicinski
2026-09-21 20:47     ` Eric Dumazet

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260921183758.1812310-6-edumazet@google.com \
    --to=edumazet@google.com \
    --cc=davem@davemloft.net \
    --cc=eric.dumazet@gmail.com \
    --cc=horms@kernel.org \
    --cc=kuba@kernel.org \
    --cc=netdev@vger.kernel.org \
    --cc=pabeni@redhat.com \
    --cc=willemb@google.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox