[PATCH net-next 0/5] net: ethtool: make netdev_rss_key_fill() spread flows over all queues

COOLING7d

15 messages, 2 authors, 7d ago · open the first message on its own page

[PATCH net-next 0/5] net: ethtool: make netdev_rss_key_fill() spread flows over all queues

From: Eric Dumazet <edumazet@google.com>
Date: 2026-09-21 18:38:01

netdev_rss_key_fill() hands drivers a uniformly random key. Because the
Toeplitz hash is linear over GF(2), a random key is singular for a given
header field and queue count with probability 1/2: flows differing only
in the low order bits of that field (such as a burst of consecutive
ephemeral ports) then cannot reach all RX queues. On one affected 16-queue
host, only 4 queues received traffic until the key was replaced.

Patch 1 synchronizes proc_do_rss_key() with netdev_rss_key_fill() using an
smp_wmb()/smp_rmb() pair and a boolean flag so readers of
/proc/sys/net/core/netdev_rss_key print zero bytes until the key is fully
populated.

Patch 2 keeps the key random but constrains 1008 of its 2048 bits so that,
at every 16-bit aligned position and for every power-of-two queue count up
to 256, consecutive values of a field ending there visit every queue once.
Applying the fixup on the 16-bit grid covers standard 2-tuple/4-tuple
hashes as well as encapsulated or bitmap-selected layouts (such as PSP
inner TCP ports at byte 78) without enumerating them in the core. The
fixup is a bijection onto the set of valid keys, and redraws in the ~1 in
5 case where two 32-bit windows a multiple of 8 bits apart alias.

Patch 3 adds a KUnit suite verifying full rank across standard and
encapsulated fields, the full 16-bit grid, window non-aliasing, and
end-to-end flow spreading.

Patches 4 and 5 add kselftests for keys on a running system: patch 4
checks /proc/sys/net/core/netdev_rss_key, and patch 5 checks the live RSS
key and indirection table reported by a device via ethtool.

Eric Dumazet (5):
  net: synchronize proc_do_rss_key() with netdev_rss_key_fill()
  net: ethtool: generate RSS keys that spread flows over all queues
  net: ethtool: add KUnit tests for the generated RSS key
  selftests: net: check the quality of the host RSS key
  selftests: drivers: net: check the RSS key a device uses

 Documentation/networking/scaling.rst          |   9 +
 net/Kconfig                                   |  14 +
 net/core/dev.h                                |   1 +
 net/core/sysctl_net_core.c                    |   7 +-
 net/ethtool/Makefile                          |   2 +
 net/ethtool/common.h                          |  11 +
 net/ethtool/ioctl.c                           | 170 +++++++++-
 net/ethtool/rss_key_test.c                    | 314 ++++++++++++++++++
 .../testing/selftests/drivers/net/hw/Makefile |   1 +
 .../drivers/net/hw/lib/py/__init__.py         |   5 +
 .../selftests/drivers/net/hw/rss_key.py       | 172 ++++++++++
 tools/testing/selftests/net/Makefile          |   1 +
 .../testing/selftests/net/lib/py/__init__.py  |   5 +
 tools/testing/selftests/net/lib/py/rsskey.py  | 108 ++++++
 tools/testing/selftests/net/netdev_rss_key.py | 117 +++++++
 15 files changed, 935 insertions(+), 2 deletions(-)
 create mode 100644 net/ethtool/rss_key_test.c
 create mode 100755 tools/testing/selftests/drivers/net/hw/rss_key.py
 create mode 100644 tools/testing/selftests/net/lib/py/rsskey.py
 create mode 100755 tools/testing/selftests/net/netdev_rss_key.py

-- 
2.55.0.1082.g2b9226bbc0-goog

[PATCH net-next 1/5] net: synchronize proc_do_rss_key() with netdev_rss_key_fill()

From: Eric Dumazet <edumazet@google.com>
Date: 2026-09-21 18:38:02

proc_do_rss_key() reads netdev_rss_key[] without synchronization, so a
concurrent reader of /proc/sys/net/core/netdev_rss_key can observe a
partially populated key while netdev_rss_key_fill() is initializing it.

Publish a boolean flag after an smp_wmb() once netdev_rss_key_fill() has
populated netdev_rss_key[], and pair it with an smp_rmb() in
proc_do_rss_key(), printing zero bytes until the flag is set.

Fixes: 960fb622f851 ("net: provide a per host RSS key generic infrastructure")
Signed-off-by: Eric Dumazet <edumazet@google.com>
---
 net/core/dev.h             | 1 +
 net/core/sysctl_net_core.c | 7 ++++++-
 net/ethtool/ioctl.c        | 7 +++++++
 3 files changed, 14 insertions(+), 1 deletion(-)
diff --git a/net/core/dev.h b/net/core/dev.h
index b757faead4d1a3e445e54d2f468c38c9e09b6762..0127b4d03e5251e0ad695aa649d0c52c2248bc8c 100644
--- a/net/core/dev.h
+++ b/net/core/dev.h
@@ -95,6 +95,7 @@ extern int		netdev_unregister_timeout_secs;
 extern int		weight_p;
 extern int		dev_weight_rx_bias;
 extern int		dev_weight_tx_bias;
+extern bool		netdev_rss_key_initialized;
 
 extern struct rw_semaphore dev_addr_sem;
 
diff --git a/net/core/sysctl_net_core.c b/net/core/sysctl_net_core.c
index 23310581f494553e3ad8c4e010c7799e04f976c0..e30219765db3991f46fdcb96236e7eaf0878a0cc 100644
--- a/net/core/sysctl_net_core.c
+++ b/net/core/sysctl_net_core.c
@@ -338,9 +338,14 @@ static int proc_do_rss_key(const struct ctl_table *table, int write,
 	char buf[NETDEV_RSS_KEY_LEN * 3];
 	struct ctl_table fake_table;
 	char *pos = buf;
+	bool initialized;
+
+	initialized = READ_ONCE(netdev_rss_key_initialized);
+	/* Pair with smp_wmb() in netdev_rss_key_fill(). */
+	smp_rmb();
 
 	for (int i = 0; i < NETDEV_RSS_KEY_LEN; i++) {
-		pos = hex_byte_pack(pos, netdev_rss_key[i]);
+		pos = hex_byte_pack(pos, initialized ? netdev_rss_key[i] : 0);
 		*pos++ = ':';
 	}
 	*(--pos) = 0;
diff --git a/net/ethtool/ioctl.c b/net/ethtool/ioctl.c
index 4b0bc503f9307880e436d5ee30f75476264a0c6b..b2820f02ca2790a8a3c13395e20040ee6976006f 100644
--- a/net/ethtool/ioctl.c
+++ b/net/ethtool/ioctl.c
@@ -34,6 +34,7 @@
 #include <net/netdev_lock.h>
 #include <net/netdev_queues.h>
 
+#include "../core/dev.h"
 #include "common.h"
 
 /* State held across locks and calls for commands which have devlink fallback */
@@ -1302,11 +1303,17 @@ static int ethtool_copy_validate_indir(u32 *indir, void __user *useraddr,
 }
 
 u8 netdev_rss_key[NETDEV_RSS_KEY_LEN] __read_mostly;
+bool netdev_rss_key_initialized __read_mostly;
 
 void netdev_rss_key_fill(void *buffer, size_t len)
 {
 	BUG_ON(len > sizeof(netdev_rss_key));
 	net_get_random_once(netdev_rss_key, sizeof(netdev_rss_key));
+	if (unlikely(!READ_ONCE(netdev_rss_key_initialized))) {
+		/* Pair with smp_rmb() in proc_do_rss_key(). */
+		smp_wmb();
+		WRITE_ONCE(netdev_rss_key_initialized, true);
+	}
 	memcpy(buffer, netdev_rss_key, len);
 }
 EXPORT_SYMBOL(netdev_rss_key_fill);
-- 
2.55.0.1082.g2b9226bbc0-goog

[PATCH net-next 2/5] net: ethtool: generate RSS keys that spread flows over all queues

From: Eric Dumazet <edumazet@google.com>
Date: 2026-09-21 18:38:04

netdev_rss_key_fill() returns a key made of uniformly random bytes. That
is not enough, because the Toeplitz hash is linear over GF(2).

Walking the hash input MSB first, each set bit contributes a 32-bit
sliding window of the key, and hardware indexes the indirection table with
the low order bits of the result. Only the tail of each window therefore
reaches the queue index:

	v(i) = key bits [i + 32 - q .. i + 31]

with q = log2(number of RX queues). Consecutive input bits give windows
overlapping in q - 1 positions, so the q vectors belonging to the q lowest
bits of a header field form a Toeplitz matrix built from 2 * q - 1 key
bits, rather than q * q independent ones. Over GF(2) a random Toeplitz
matrix is singular with probability exactly 1/2, whatever its size.

When it is singular, flows differing only in the low order bits of that
field cannot reach all the queues. This is not theoretical: a burst of
connections draws ephemeral ports from a narrow range, and on one affected
host only 4 of the 16 RX queues received any traffic at all, until its key
was replaced.

Keep drawing the key at random, since it is a secret that stops a remote
attacker from steering flows onto a single queue, but force the handful of
bits that decide this. Writing d[t] for key bit (lsb + 31 - t), where lsb
is the position of the least significant bit of a field in the hash input,
the matrices of all the q values up to 8 are non singular if and only if

	d[2 * i] = 1 ^ d[i] ^ d[i + 1] ^ ... ^ d[2 * i - 1]

The odd positions stay free, so this is a one pass fixup rather than a
search. It is in fact a bijection from those free positions onto the set
of the values having the property, so the key stays uniformly distributed
over that set and the whole cost is 8 bits of entropy per position.
Searching for such a key by rejection would not have been an option: a
freshly drawn one has the property everywhere with probability 2^-1008.

Apply this at every 16-bit aligned position of the key, rather than at the
offsets of the 2-tuple and 4-tuple layouts only. The core does not get to
know what a given NIC hashes. Hardware may select the bytes it feeds to
Toeplitz out of a header window with a bitmap, and hash an encapsulated
header: for PSP over UDP over IPv6 it can pick the outer addresses and the
inner TCP ports, which sit 78 bytes into the frame and read key bits well
past the 40 bytes an IPv6 4-tuple needs. Hashed fields are 16 bits wide at
the smallest and are not expected to straddle that grid, so covering the
grid covers the layouts that were never written down, at no cost in code.

This spends 8 bits of entropy per position, 1008 bits out of the 2048 bits
of netdev_rss_key, leaving 1040 bits. 8 is also the largest usable bound,
as each q constrains 2 * q - 1 bits and anything larger would make the
ranges of two adjacent positions overlap.

What the fixup leaves behind is visible structure: 8 of every 16 bits are
derived from the 8 others, so a 16-bit aligned word of the key takes only
2^8 values and about 27 of the 128 words of netdev_rss_key duplicate
another one. That much is forced rather than an artefact of this
implementation, 1040 bits spread over 128 words being a little over 8 bits
each, but it has one consequence worth removing. Two 32-bit windows a
whole number of words apart now collide with probability 2^-16 instead of
2^-32, and two input bits reading the same window are indistinguishable to
the hash, since flipping both of them leaves it unchanged. Over 500 keys,
23% of them had such a pair, where a uniformly random key has one with
probability 2^-11.

So draw another key when that happens. Four out of five pass. Which
distances to look at follows from where the structure is: covering the
multiples of 16 leaves 1.2e-3 expected colliding pairs per key, still 2.6
times the 4.6e-4 of a plain random key, and almost all of that excess sits
at a distance of 8 modulo 16. Covering every multiple of 8 brings the total
down to 4.2e-4, below what a plain random key gives over all distances, and
within a few percent of the 4.0e-4 it gives over the distances that are
left.

Two windows a multiple of 8 bits apart are two windows at the same offset
modulo 8, so this is a handful of pairwise sweeps rather than one pass over
the key per distance. DO_ONCE() runs the generator under a spinlock with
hard IRQs disabled, so keep the windows of a class in an array rather than
recomputing both sides of every pair: 116 us instead of 254 us for the
worst case, a 2048-bit key with no collision anywhere, at a cost of 512
bytes of stack.

The shared key is fixed up once and every driver prefix inherits both
properties.

Checked against an independent Toeplitz implementation: for every 16-bit
aligned position and every q in 1..8, an aligned block of 2^q consecutive
values of a field ending there lands on the 2^q queues exactly once each.
Over 20 random keys that is 20160 checks, which 50.1% of plain random keys
fail and none of the generated keys do, for an average of 502 rewritten
bits out of 2048.

Signed-off-by: Eric Dumazet <edumazet@google.com>
---
 Documentation/networking/scaling.rst |   9 ++
 net/ethtool/ioctl.c                  | 172 ++++++++++++++++++++++++++-
 2 files changed, 175 insertions(+), 6 deletions(-)
diff --git a/Documentation/networking/scaling.rst b/Documentation/networking/scaling.rst
index 6c261eb48845a40516f201233df13694863ee8cd..6c9836000a8d15c209d52fe78e46d346156893e4 100644
--- a/Documentation/networking/scaling.rst
+++ b/Documentation/networking/scaling.rst
@@ -48,6 +48,15 @@ count is not a power of two. NICs should provide an indirection table
 at least 4 times larger than the queue count. 4x table results in ~16%
 imbalance between the queues, which is acceptable for most applications.
 
+The Toeplitz hash is linear over GF(2), so the quality of the hash key
+matters as much as its randomness. For a key made of uniformly random
+bytes, the q lowest order bits of a given header field fail to spread
+flows over all 2^q queues with probability 1/2, and a burst of connections
+picking nearly consecutive ephemeral ports then lands on a fraction of the
+queues while the others stay idle. The netdev_rss_key_fill() helper draws
+a random key that is free of this defect; drivers should use it rather
+than seeding a key of their own.
+
 Some NICs support symmetric RSS hashing where, if the IP (source address,
 destination address) and TCP/UDP (source port, destination port) tuples
 are swapped, the computed hash is the same. This is beneficial in some
diff --git a/net/ethtool/ioctl.c b/net/ethtool/ioctl.c
index b2820f02ca2790a8a3c13395e20040ee6976006f..bdc40cfd27217d85b4aa2ac93d0b70fc8d22371a 100644
--- a/net/ethtool/ioctl.c
+++ b/net/ethtool/ioctl.c
@@ -25,7 +25,10 @@
 #include <linux/rtnetlink.h>
 #include <linux/sched/signal.h>
 #include <linux/net.h>
+#include <linux/once.h>
 #include <linux/pm_runtime.h>
+#include <linux/random.h>
+#include <linux/unaligned.h>
 #include <linux/utsname.h>
 #include <linux/ethtool_netlink.h>
 #include <net/devlink.h>
@@ -1305,15 +1308,172 @@ static int ethtool_copy_validate_indir(u32 *indir, void __user *useraddr,
 u8 netdev_rss_key[NETDEV_RSS_KEY_LEN] __read_mostly;
 bool netdev_rss_key_initialized __read_mostly;
 
+/* Toeplitz is linear over GF(2): the hash is the XOR of the 32-bit key
+ * windows selected by the set bits of the input, and hardware indexes the
+ * indirection table with the low order bits of the hash. Only the tail of
+ * each window therefore matters for queue selection:
+ *
+ *	v(i) = key bits [i + 32 - q .. i + 31]
+ *
+ * for input bit @i, with q = log2(number of RX queues). Consecutive input
+ * bits give windows overlapping in q - 1 positions, so the matrix formed by
+ * the windows of the q lowest bits of a header field is a Toeplitz matrix
+ * built from 2 * q - 1 key bits, not from q * q independent ones. A random
+ * Toeplitz matrix over GF(2) is singular with probability 1/2, whatever its
+ * size, and when it is singular the flows of a burst differing only in the
+ * low order bits of that field (consecutive ephemeral ports, typically)
+ * cannot reach all the RX queues no matter how many of them are configured.
+ *
+ * Keep the key random, but constrain the few bits that decide this. The
+ * fixup is applied at every 16-bit aligned position of the key, rather than
+ * at the offsets of the one hash input layout the software happens to know
+ * about: hardware is free to hash whatever it wants, but the fields it picks
+ * are 16 bits wide at the smallest and are not expected to straddle that
+ * grid, so an encapsulated or offloaded layout is covered like the usual
+ * 2-tuple and 4-tuple ones. Each position constrains 2 * q - 1 bits, so
+ * NETDEV_RSS_KEY_QMAX is both enough for 256 queues and the largest value
+ * keeping the ranges of two adjacent positions disjoint.
+ */
+#define NETDEV_RSS_KEY_QMAX	8
+#define NETDEV_RSS_KEY_SPAN	(2 * NETDEV_RSS_KEY_QMAX - 1)
+
+static bool netdev_rss_key_bit(const u8 *key, unsigned int bit)
+{
+	return key[bit / BITS_PER_BYTE] & (0x80 >> (bit % BITS_PER_BYTE));
+}
+
+static void netdev_rss_key_assign_bit(u8 *key, unsigned int bit, bool value)
+{
+	u8 mask = 0x80 >> (bit % BITS_PER_BYTE);
+
+	if (value)
+		key[bit / BITS_PER_BYTE] |= mask;
+	else
+		key[bit / BITS_PER_BYTE] &= ~mask;
+}
+
+/* Writing d[t] for key bit (@lsb + 31 - t), the matrices of all the q values
+ * up to NETDEV_RSS_KEY_QMAX are non singular if and only if
+ *
+ *	d[2 * i] = 1 ^ d[i] ^ d[i + 1] ^ ... ^ d[2 * i - 1]
+ *
+ * The odd positions stay free, so this is a one pass fixup rather than a
+ * search. It is also a bijection onto the set of the values having the
+ * property, so the key stays uniformly distributed over that set. It costs
+ * NETDEV_RSS_KEY_QMAX bits of entropy per position.
+ */
+static void netdev_rss_key_fixup_field(u8 *key, unsigned int lsb)
+{
+	bool d[NETDEV_RSS_KEY_SPAN];
+	unsigned int i, t;
+
+	for (t = 0; t < NETDEV_RSS_KEY_SPAN; t++)
+		d[t] = netdev_rss_key_bit(key, lsb + 31 - t);
+
+	for (i = 0; 2 * i < NETDEV_RSS_KEY_SPAN; i++) {
+		bool value = true;
+
+		for (t = i; t < 2 * i; t++)
+			value ^= d[t];
+
+		d[2 * i] = value;
+	}
+
+	for (t = 0; t < NETDEV_RSS_KEY_SPAN; t++)
+		netdev_rss_key_assign_bit(key, lsb + 31 - t, d[t]);
+}
+
+/* The 32 key bits starting at @bit, which is what input bit @bit contributes
+ * to the hash. @bit + 32 must fit in the key.
+ */
+static u32 netdev_rss_key_window(const u8 *key, unsigned int bit)
+{
+	unsigned int byte = bit / BITS_PER_BYTE;
+	unsigned int shift = bit % BITS_PER_BYTE;
+	u32 window = get_unaligned_be32(key + byte);
+
+	if (shift)
+		window = (window << shift) |
+			 (key[byte + 4] >> (BITS_PER_BYTE - shift));
+
+	return window;
+}
+
+/* Two input bits contributing the same window are indistinguishable to the
+ * hash, since flipping both of them leaves it unchanged. For a uniformly
+ * random key that is a 2 ** -32 event per pair of positions, but the fixup
+ * makes it likelier: it derives 8 of every 16 bits from the 8 others, so a
+ * 16-bit aligned word only takes 2 ** 8 values and two windows a whole
+ * number of words apart collide with probability 2 ** -16 instead. Half a
+ * word apart is less affected but still well clear of the random odds, so
+ * cover every distance that is a multiple of 8. What is left after that is
+ * below what a plain random key gives.
+ *
+ * Two windows at a distance that is a multiple of 8 are two windows at the
+ * same offset modulo 8. Caching a whole class would put 253 u32 on the
+ * stack, so cache one class modulo 16 and stream the class 8 bits above it
+ * against it.
+ */
+static bool netdev_rss_key_aliases(const u8 *key, unsigned int bits)
+{
+	u32 windows[NETDEV_RSS_KEY_LEN * BITS_PER_BYTE / 16];
+	unsigned int i, j, n, r;
+
+	for (r = 0; r < 16; r++) {
+		n = 0;
+		for (i = r; i + 32 <= bits; i += 16)
+			windows[n++] = netdev_rss_key_window(key, i);
+
+		for (i = 0; i < n; i++)
+			for (j = i + 1; j < n; j++)
+				if (windows[i] == windows[j])
+					return true;
+
+		if (r >= 8)
+			continue;
+
+		for (i = r + 8; i + 32 <= bits; i += 16) {
+			u32 window = netdev_rss_key_window(key, i);
+
+			for (j = 0; j < n; j++)
+				if (windows[j] == window)
+					return true;
+		}
+	}
+
+	return false;
+}
+
+static void netdev_rss_key_init(u8 *key, size_t len)
+{
+	unsigned int lsb, bits = len * BITS_PER_BYTE;
+
+	/* Four keys out of five come out of the fixup free of aliases, so
+	 * drawing another one is both simpler and cheaper than repairing.
+	 */
+	do {
+		get_random_bytes(key, len);
+
+		/* A field ending at bit @lsb uses key bits [.. , @lsb + 31],
+		 * so stop as soon as a 32-bit window no longer fits in the
+		 * key.
+		 */
+		for (lsb = 15; lsb + 32 <= bits; lsb += 16)
+			netdev_rss_key_fixup_field(key, lsb);
+	} while (netdev_rss_key_aliases(key, bits));
+
+	if (key != netdev_rss_key)
+		return;
+
+	/* Pair with smp_rmb() in proc_do_rss_key(). */
+	smp_wmb();
+	WRITE_ONCE(netdev_rss_key_initialized, true);
+}
+
 void netdev_rss_key_fill(void *buffer, size_t len)
 {
 	BUG_ON(len > sizeof(netdev_rss_key));
-	net_get_random_once(netdev_rss_key, sizeof(netdev_rss_key));
-	if (unlikely(!READ_ONCE(netdev_rss_key_initialized))) {
-		/* Pair with smp_rmb() in proc_do_rss_key(). */
-		smp_wmb();
-		WRITE_ONCE(netdev_rss_key_initialized, true);
-	}
+	DO_ONCE(netdev_rss_key_init, netdev_rss_key, sizeof(netdev_rss_key));
 	memcpy(buffer, netdev_rss_key, len);
 }
 EXPORT_SYMBOL(netdev_rss_key_fill);
-- 
2.55.0.1082.g2b9226bbc0-goog

[PATCH net-next 3/5] net: ethtool: add KUnit tests for the generated RSS key

From: Eric Dumazet <edumazet@google.com>
Date: 2026-09-21 18:38:05

Check the property from the definition of the Toeplitz hash, independently
of the way netdev_rss_key_init() achieves it: an aligned block of 2^q
consecutive values of one field has to land on the 2^q queues exactly once
each.

Four checks and a control. rss_key_property_test() does the algebra for
the named fields of the usual hash inputs, rss_key_grid_test() sweeps every
16-bit aligned position of the key, since the generator does not get to
know the layout the hardware uses, and rss_key_spread_test() hashes the
inputs of an actual burst and looks at where they land. The control,
rss_key_checker_test(), feeds degenerate keys to the rank check so that a
check accepting everything cannot make the others pass.

rss_key_alias_test() covers the other half of what the generator promises,
that no two input bits read the same 32-bit key window and are therefore
indistinguishable to the hash. It sorts the windows instead of comparing
them pairwise, so unlike netdev_rss_key_init() it looks at every distance
rather than at the multiples of 16 alone.

The field table includes a PSP over UDP over IPv6 layout, whose inner TCP
ports sit far past the plain 4-tuple, because that is the case the offsets
of the standard layouts do not cover.

Commenting out the fixup makes three of the five cases fail and leaves the
control passing. Keeping the fixup but skipping the redraw fails
rss_key_alias_test alone.

Signed-off-by: Eric Dumazet <edumazet@google.com>
---
 net/Kconfig                |  14 ++
 net/ethtool/Makefile       |   2 +
 net/ethtool/common.h       |  11 ++
 net/ethtool/ioctl.c        |   5 +-
 net/ethtool/rss_key_test.c | 314 +++++++++++++++++++++++++++++++++++++
 5 files changed, 344 insertions(+), 2 deletions(-)
 create mode 100644 net/ethtool/rss_key_test.c
diff --git a/net/Kconfig b/net/Kconfig
index e384773935515d54ab05ec71b3d2792347428ad7..4a8dc0e5b075a1b1c535d940a80cb8976fe47694 100644
--- a/net/Kconfig
+++ b/net/Kconfig
@@ -540,4 +540,18 @@ config NET_TEST
 
 	  If unsure, say N.
 
+config ETHTOOL_RSS_KEY_KUNIT_TEST
+	tristate "KUnit tests for the generated RSS key" if !KUNIT_ALL_TESTS
+	depends on KUNIT
+	default KUNIT_ALL_TESTS
+	help
+	  KUnit tests checking the RSS key that netdev_rss_key_fill() hands
+	  to the drivers: flows differing only in the low order bits of one
+	  hashed header field have to spread over all the RX queues, for
+	  every field of the Toeplitz hash inputs, for every 16-bit aligned
+	  position of the key, and for every queue count up to
+	  2 ** NETDEV_RSS_KEY_QMAX.
+
+	  If unsure, say N.
+
 endif   # if NET
diff --git a/net/ethtool/Makefile b/net/ethtool/Makefile
index 629c10916670ecc2dc5f3dbb7091c1c126679975..6588cfe652810e3bbbcc78185ad1a8bd68ae92e5 100644
--- a/net/ethtool/Makefile
+++ b/net/ethtool/Makefile
@@ -10,3 +10,5 @@ ethtool_nl-y	:= netlink.o bitset.o strset.o linkinfo.o linkmodes.o rss.o \
 		   tunnels.o fec.o eeprom.o stats.o phc_vclocks.o mm.o \
 		   module.o cmis_fw_update.o cmis_cdb.o pse-pd.o plca.o \
 		   phy.o tsconfig.o mse.o
+
+obj-$(CONFIG_ETHTOOL_RSS_KEY_KUNIT_TEST)	+= rss_key_test.o
diff --git a/net/ethtool/common.h b/net/ethtool/common.h
index 4e5356e26f400aa0a303e54c072bc0119f11513b..c1b6191f456935d2e3994ac6e12605e1f30693d4 100644
--- a/net/ethtool/common.h
+++ b/net/ethtool/common.h
@@ -15,6 +15,17 @@
 #define __SOF_TIMESTAMPING_CNT (const_ilog2(SOF_TIMESTAMPING_LAST) + 1)
 #define __HWTSTAMP_FLAG_CNT (const_ilog2(HWTSTAMP_FLAG_LAST) + 1)
 
+/* netdev_rss_key_fill() guarantees that flows differing only in the low order
+ * bits of one hashed header field spread over all the RX queues, for any queue
+ * count up to 2 ** NETDEV_RSS_KEY_QMAX and any 16-bit aligned field of the
+ * hash input. See netdev_rss_key_init().
+ */
+#define NETDEV_RSS_KEY_QMAX	8
+
+#if IS_ENABLED(CONFIG_KUNIT)
+void netdev_rss_key_init(u8 *key, size_t len);
+#endif
+
 struct genl_info;
 struct hwtstamp_provider_desc;
 
diff --git a/net/ethtool/ioctl.c b/net/ethtool/ioctl.c
index bdc40cfd27217d85b4aa2ac93d0b70fc8d22371a..e625175ed1cbdd540e290967ece2d0bdaab1b00f 100644
--- a/net/ethtool/ioctl.c
+++ b/net/ethtool/ioctl.c
@@ -31,6 +31,7 @@
 #include <linux/unaligned.h>
 #include <linux/utsname.h>
 #include <linux/ethtool_netlink.h>
+#include <kunit/visibility.h>
 #include <net/devlink.h>
 #include <net/ipv6.h>
 #include <net/flow_offload.h>
@@ -1334,7 +1335,6 @@ bool netdev_rss_key_initialized __read_mostly;
  * NETDEV_RSS_KEY_QMAX is both enough for 256 queues and the largest value
  * keeping the ranges of two adjacent positions disjoint.
  */
-#define NETDEV_RSS_KEY_QMAX	8
 #define NETDEV_RSS_KEY_SPAN	(2 * NETDEV_RSS_KEY_QMAX - 1)
 
 static bool netdev_rss_key_bit(const u8 *key, unsigned int bit)
@@ -1444,7 +1444,7 @@ static bool netdev_rss_key_aliases(const u8 *key, unsigned int bits)
 	return false;
 }
 
-static void netdev_rss_key_init(u8 *key, size_t len)
+VISIBLE_IF_KUNIT void netdev_rss_key_init(u8 *key, size_t len)
 {
 	unsigned int lsb, bits = len * BITS_PER_BYTE;
 
@@ -1469,6 +1469,7 @@ static void netdev_rss_key_init(u8 *key, size_t len)
 	smp_wmb();
 	WRITE_ONCE(netdev_rss_key_initialized, true);
 }
+EXPORT_SYMBOL_IF_KUNIT(netdev_rss_key_init);
 
 void netdev_rss_key_fill(void *buffer, size_t len)
 {
diff --git a/net/ethtool/rss_key_test.c b/net/ethtool/rss_key_test.c
new file mode 100644
index 0000000000000000000000000000000000000000..1a583f2930c5ba92a64a349c291bb6922c44998d
--- /dev/null
+++ b/net/ethtool/rss_key_test.c
@@ -0,0 +1,314 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/* Tests for the RSS key generated by netdev_rss_key_fill().
+ *
+ * The property under test is that flows differing only in the low order bits
+ * of one hashed header field land on distinct RX queues. It is checked here
+ * from the definition of the Toeplitz hash, independently of the way
+ * netdev_rss_key_init() achieves it.
+ */
+
+#include <kunit/test.h>
+#include <linux/module.h>
+#include <linux/random.h>
+#include <linux/sort.h>
+
+#include "common.h"
+
+MODULE_IMPORT_NS("EXPORTED_FOR_KUNIT_TESTING");
+
+/* Longest hash input exercised here: an IPv6 header, a UDP header, a PSP
+ * header and the inner TCP ports. See rss_key_test_fields[].
+ */
+#define RSS_KEY_TEST_INPUT_LEN		68
+
+#define RSS_KEY_TEST_KEYS		32
+
+/* Number of queue counts exercised end to end by rss_key_spread_test(), which
+ * hashes 2 ** q inputs for each of them. The algebraic check covers all the
+ * queue counts up to NETDEV_RSS_KEY_QMAX.
+ */
+#define RSS_KEY_TEST_SPREAD_QMAX	6
+
+/* Position of the least significant bit of each field, counting from the most
+ * significant bit of the hash input, and the length of that input. These come
+ * from the definition of the hash, not from the key generator.
+ */
+static const struct rss_key_test_field {
+	const char	*name;
+	unsigned int	lsb;
+	unsigned int	nbits;
+} rss_key_test_fields[] = {
+	{ "IPv4 saddr",		31,	96 },
+	{ "IPv4 daddr",		63,	96 },
+	{ "IPv4 sport",		79,	96 },
+	{ "IPv4 dport",		95,	96 },
+	{ "IPv6 saddr",		127,	288 },
+	{ "IPv6 daddr",		255,	288 },
+	{ "IPv6 sport",		271,	288 },
+	{ "IPv6 dport",		287,	288 },
+	/* Hardware is free to hash an encapsulated header instead, and then
+	 * it reads the key far past the 40 bytes an IPv6 4-tuple needs. These
+	 * offsets are those of PSP transport mode over UDP over IPv6
+	 * (Documentation/networking/psp.rst): 40 bytes of IPv6, 8 of UDP,
+	 * PSP_HDR_SIZE of PSP, then the inner TCP ports. Prepending an
+	 * Ethernet header, or the 4 extra bytes of an encapsulation tag,
+	 * shifts all of this by a whole number of 16-bit units.
+	 */
+	{ "PSP outer IPv6 saddr",	191,	544 },
+	{ "PSP outer IPv6 daddr",	319,	544 },
+	{ "PSP inner sport",		527,	544 },
+	{ "PSP inner dport",		543,	544 },
+};
+
+static bool rss_key_test_bit(const u8 *buf, unsigned int bit)
+{
+	return buf[bit / BITS_PER_BYTE] & (0x80 >> (bit % BITS_PER_BYTE));
+}
+
+static void rss_key_test_assign_bit(u8 *buf, unsigned int bit, bool value)
+{
+	u8 mask = 0x80 >> (bit % BITS_PER_BYTE);
+
+	if (value)
+		buf[bit / BITS_PER_BYTE] |= mask;
+	else
+		buf[bit / BITS_PER_BYTE] &= ~mask;
+}
+
+/* The 32 key bits starting at @bit, which is what input bit @bit contributes
+ * to the hash.
+ */
+static u32 rss_key_test_window(const u8 *key, unsigned int bit)
+{
+	u32 window = 0;
+	unsigned int i;
+
+	for (i = 0; i < 32; i++)
+		window = (window << 1) | rss_key_test_bit(key, bit + i);
+
+	return window;
+}
+
+static u32 rss_key_test_toeplitz(const u8 *key, const u8 *input,
+				 unsigned int nbits)
+{
+	u32 hash = 0;
+	unsigned int i;
+
+	for (i = 0; i < nbits; i++)
+		if (rss_key_test_bit(input, i))
+			hash ^= rss_key_test_window(key, i);
+
+	return hash;
+}
+
+/* Is the map from the q low order bits of the field at @lsb to the q low order
+ * bits of the hash a bijection? Gaussian elimination over GF(2) on the q
+ * windows involved, reduced to their q low order bits.
+ */
+static bool rss_key_test_full_rank(const u8 *key, unsigned int lsb,
+				   unsigned int q)
+{
+	u32 basis[NETDEV_RSS_KEY_QMAX] = {};
+	unsigned int j;
+
+	for (j = 0; j < q; j++) {
+		u32 v = rss_key_test_window(key, lsb - j) & (BIT(q) - 1);
+
+		while (v) {
+			unsigned int b = __ffs(v);
+
+			if (!basis[b]) {
+				basis[b] = v;
+				break;
+			}
+			v ^= basis[b];
+		}
+
+		if (!v)
+			return false;
+	}
+
+	return true;
+}
+
+/* Degenerate keys the rank check must reject, so that a check accepting
+ * everything can not make the other tests pass.
+ */
+static void rss_key_checker_test(struct kunit *test)
+{
+	u8 *key = kunit_kzalloc(test, NETDEV_RSS_KEY_LEN, GFP_KERNEL);
+
+	KUNIT_ASSERT_NOT_NULL(test, key);
+
+	/* All the windows are zero. */
+	KUNIT_EXPECT_FALSE(test, rss_key_test_full_rank(key, 31, 1));
+
+	/* All the windows are equal, which is enough for one queue only. */
+	memset(key, 0xff, NETDEV_RSS_KEY_LEN);
+	KUNIT_EXPECT_TRUE(test, rss_key_test_full_rank(key, 31, 1));
+	KUNIT_EXPECT_FALSE(test, rss_key_test_full_rank(key, 31, 2));
+}
+
+static void rss_key_property_test(struct kunit *test)
+{
+	u8 *key = kunit_kzalloc(test, NETDEV_RSS_KEY_LEN, GFP_KERNEL);
+	unsigned int i, j, q;
+
+	KUNIT_ASSERT_NOT_NULL(test, key);
+
+	for (i = 0; i < RSS_KEY_TEST_KEYS; i++) {
+		netdev_rss_key_init(key, NETDEV_RSS_KEY_LEN);
+
+		for (j = 0; j < ARRAY_SIZE(rss_key_test_fields); j++) {
+			const struct rss_key_test_field *f;
+
+			f = &rss_key_test_fields[j];
+
+			for (q = 1; q <= NETDEV_RSS_KEY_QMAX; q++)
+				KUNIT_ASSERT_TRUE_MSG(test,
+						      rss_key_test_full_rank(key, f->lsb, q),
+						      "%s does not spread over %u queues",
+						      f->name, 1U << q);
+		}
+	}
+}
+
+/* rss_key_test_fields[] can only list the layouts somebody thought of, but
+ * the generator does not get to know what the hardware hashes. Sweep the
+ * whole key instead: the property has to hold at every 16-bit aligned
+ * position, which is where a field of any layout can end.
+ */
+static void rss_key_grid_test(struct kunit *test)
+{
+	unsigned int bits = NETDEV_RSS_KEY_LEN * BITS_PER_BYTE;
+	u8 *key = kunit_kzalloc(test, NETDEV_RSS_KEY_LEN, GFP_KERNEL);
+	unsigned int i, lsb, q;
+
+	KUNIT_ASSERT_NOT_NULL(test, key);
+
+	for (i = 0; i < RSS_KEY_TEST_KEYS; i++) {
+		netdev_rss_key_init(key, NETDEV_RSS_KEY_LEN);
+
+		for (lsb = 15; lsb + 32 <= bits; lsb += 16)
+			for (q = 1; q <= NETDEV_RSS_KEY_QMAX; q++)
+				KUNIT_ASSERT_TRUE_MSG(test,
+						      rss_key_test_full_rank(key, lsb, q),
+						      "field ending at bit %u does not spread over %u queues",
+						      lsb, 1U << q);
+	}
+}
+
+static int rss_key_test_cmp(const void *a, const void *b)
+{
+	u32 x = *(const u32 *)a, y = *(const u32 *)b;
+
+	return x < y ? -1 : x > y;
+}
+
+/* Two input bits contributing the same 32-bit window are indistinguishable
+ * to the hash: flipping both of them leaves it unchanged, so the flows of a
+ * burst differing in exactly those two bits all collide. A uniformly random
+ * key has such a pair with probability 2 ** -11, and the generated key must
+ * not do worse.
+ *
+ * netdev_rss_key_init() redraws on a collision at a distance that is a
+ * multiple of 8, which is where the fixup makes one likely, and leaves every
+ * other distance at the odds of a random key. So assert on what it
+ * guarantees: group the windows by their offset modulo 8 and sort each group
+ * on its own. Sorting all of them together instead would be asserting on the
+ * random odds, and would fail about one run in 26 on a correct kernel.
+ */
+static void rss_key_alias_test(struct kunit *test)
+{
+	unsigned int count = NETDEV_RSS_KEY_LEN * BITS_PER_BYTE - 31;
+	u8 *key = kunit_kzalloc(test, NETDEV_RSS_KEY_LEN, GFP_KERNEL);
+	unsigned int i, j, n, r;
+	u32 *windows;
+
+	windows = kunit_kcalloc(test, count, sizeof(*windows), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, key);
+	KUNIT_ASSERT_NOT_NULL(test, windows);
+
+	for (i = 0; i < RSS_KEY_TEST_KEYS; i++) {
+		netdev_rss_key_init(key, NETDEV_RSS_KEY_LEN);
+
+		for (r = 0; r < 8; r++) {
+			n = 0;
+			for (j = r; j < count; j += 8)
+				windows[n++] = rss_key_test_window(key, j);
+
+			sort(windows, n, sizeof(*windows),
+			     rss_key_test_cmp, NULL);
+
+			for (j = 1; j < n; j++)
+				KUNIT_ASSERT_NE_MSG(test, windows[j],
+						    windows[j - 1],
+						    "two input bits a multiple of 8 apart read the same key window %08x",
+						    windows[j]);
+		}
+	}
+}
+
+/* Hash the inputs of a burst differing only in the low order bits of one
+ * field, and check that they fill the indirection table evenly.
+ */
+static void rss_key_spread_test(struct kunit *test)
+{
+	u8 *key = kunit_kzalloc(test, NETDEV_RSS_KEY_LEN, GFP_KERNEL);
+	u8 *input = kunit_kzalloc(test, RSS_KEY_TEST_INPUT_LEN, GFP_KERNEL);
+	unsigned int i, j, q;
+
+	KUNIT_ASSERT_NOT_NULL(test, key);
+	KUNIT_ASSERT_NOT_NULL(test, input);
+
+	netdev_rss_key_init(key, NETDEV_RSS_KEY_LEN);
+
+	for (i = 0; i < ARRAY_SIZE(rss_key_test_fields); i++) {
+		const struct rss_key_test_field *f = &rss_key_test_fields[i];
+
+		for (q = 1; q <= RSS_KEY_TEST_SPREAD_QMAX; q++) {
+			u64 seen = 0;
+
+			get_random_bytes(input, RSS_KEY_TEST_INPUT_LEN);
+
+			for (j = 0; j < (1U << q); j++) {
+				unsigned int t, queue;
+				u32 hash;
+
+				for (t = 0; t < q; t++)
+					rss_key_test_assign_bit(input,
+								f->lsb - t,
+								j & BIT(t));
+
+				hash = rss_key_test_toeplitz(key, input,
+							     f->nbits);
+				queue = hash & (BIT(q) - 1);
+
+				KUNIT_ASSERT_FALSE_MSG(test, seen & BIT_ULL(queue),
+						       "%s hits queue %u twice out of %u",
+						       f->name, queue, 1U << q);
+				seen |= BIT_ULL(queue);
+			}
+		}
+	}
+}
+
+static struct kunit_case rss_key_test_cases[] = {
+	KUNIT_CASE(rss_key_checker_test),
+	KUNIT_CASE(rss_key_property_test),
+	KUNIT_CASE(rss_key_grid_test),
+	KUNIT_CASE(rss_key_alias_test),
+	KUNIT_CASE(rss_key_spread_test),
+	{},
+};
+
+static struct kunit_suite rss_key_test_suite = {
+	.name = "ethtool-rss-key",
+	.test_cases = rss_key_test_cases,
+};
+
+kunit_test_suite(rss_key_test_suite);
+
+MODULE_DESCRIPTION("Tests for the RSS key generated by netdev_rss_key_fill()");
+MODULE_LICENSE("GPL");
-- 
2.55.0.1082.g2b9226bbc0-goog

[PATCH net-next 4/5] selftests: net: check the quality of the host RSS key

From: Eric Dumazet <edumazet@google.com>
Date: 2026-09-21 18:38:07

Add a helper library for the property netdev_rss_key_fill() now provides,
and a test checking the key the running kernel has handed out, as seen in
/proc/sys/net/core/netdev_rss_key.

The library reads back an RSS key and answers, for a position in the hash
input and a queue count, whether flows differing only in the low order bits
of a field ending there reach all the queues. It derives the position of
the named fields from the flow hash configuration rather than assuming a
4-tuple, so it describes 2-tuple hashing as well.

check_spread() uses that to report the named fields, and check_grid()
sweeps every 16-bit aligned position of the key, which is what the
generator actually promises and what a NIC hashing an encapsulated header
relies on.

This complements the KUnit tests of the generator: it does not know how the
key was made, only what it has to look like. The key is generated the first
time a driver asks for it, so the test skips on a machine whose NICs never
did.

Signed-off-by: Eric Dumazet <edumazet@google.com>
---
 tools/testing/selftests/net/Makefile          |   1 +
 .../testing/selftests/net/lib/py/__init__.py  |   5 +
 tools/testing/selftests/net/lib/py/rsskey.py  | 108 ++++++++++++++++
 tools/testing/selftests/net/netdev_rss_key.py | 117 ++++++++++++++++++
 4 files changed, 231 insertions(+)
 create mode 100644 tools/testing/selftests/net/lib/py/rsskey.py
 create mode 100755 tools/testing/selftests/net/netdev_rss_key.py
diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile
index 3ee3378f8b26eff9d99a834491ad5ef594dcd782..cba6084914af5de3e8d33005484379e489c0c953 100644
--- a/tools/testing/selftests/net/Makefile
+++ b/tools/testing/selftests/net/Makefile
@@ -67,6 +67,7 @@ TEST_PROGS := \
 	nat6to4.sh \
 	ndisc_unsolicited_na_test.sh \
 	netdev-l2addr.sh \
+	netdev_rss_key.py \
 	netdevice.sh \
 	netns-name.sh \
 	netns-sysctl.sh \
diff --git a/tools/testing/selftests/net/lib/py/__init__.py b/tools/testing/selftests/net/lib/py/__init__.py
index 71df5880b356a1ea3e013891d90372b04e070d02..7648ab6c2adae659d23e32cb33dfb0341afa326e 100644
--- a/tools/testing/selftests/net/lib/py/__init__.py
+++ b/tools/testing/selftests/net/lib/py/__init__.py
@@ -16,6 +16,8 @@ from .utils import CmdExitFailure, fd_read_timeout, cmd, bkg, defer, \
     bpftool, ip, ethtool, bpftrace, rand_port, rand_ports, wait_port_listen, \
     ctl_file_write, wait_file, tool, tc
 from .bpf import bpf_map_set, bpf_map_dump, bpf_prog_map_ids
+from .rsskey 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 .ynl import NlError, NlctrlFamily, YnlFamily, \
     EthtoolFamily, NetdevFamily, RtnlFamily, RtnlAddrFamily, RtnlRouteFamily
 from .ynl import NetshaperFamily, DevlinkFamily, PSPFamily, Netlink
@@ -31,6 +33,9 @@ __all__ = ["KSRC",
            "bpftool", "ip", "ethtool", "bpftrace", "rand_port", "rand_ports",
            "wait_port_listen", "ctl_file_write", "wait_file", "tool", "tc",
            "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",
            "NetdevSim", "NetdevSimDev",
            "NetshaperFamily", "DevlinkFamily", "PSPFamily", "NlError",
            "YnlFamily", "EthtoolFamily", "NetdevFamily", "RtnlFamily",
diff --git a/tools/testing/selftests/net/lib/py/rsskey.py b/tools/testing/selftests/net/lib/py/rsskey.py
new file mode 100644
index 0000000000000000000000000000000000000000..4451eb80fb4577c5a8cce4dd9e973e4eabb2d666
--- /dev/null
+++ b/tools/testing/selftests/net/lib/py/rsskey.py
@@ -0,0 +1,108 @@
+# SPDX-License-Identifier: GPL-2.0
+
+"""
+Helpers to check the quality of an RSS key.
+
+The Toeplitz hash is linear over GF(2): the hash is the XOR of the 32 bit key
+windows selected by the set bits of the input, and hardware indexes the
+indirection table with the low order bits of the hash. The windows belonging
+to the q lowest bits of a header field therefore form a Toeplitz matrix, and
+when that matrix is singular the flows of a burst differing only in those
+bits, consecutive ephemeral ports typically, can not reach all of the 2 ** q
+entries of the table. A key drawn uniformly at random is singular for a given
+field and a given q with probability 1/2.
+
+netdev_rss_key_fill() generates keys that are non singular for every field of
+the hash input and every q up to RSS_KEY_QMAX.
+"""
+
+# Matches NETDEV_RSS_KEY_QMAX, that is up to 256 entries of the table.
+RSS_KEY_QMAX = 8
+
+
+def rss_key_bit(buf, bit):
+    """Bit @bit of @buf, counting from the most significant bit of byte 0."""
+    return (buf[bit // 8] >> (7 - bit % 8)) & 1
+
+
+def rss_key_assign_bit(buf, bit, value):
+    mask = 0x80 >> (bit % 8)
+
+    if value:
+        buf[bit // 8] |= mask
+    else:
+        buf[bit // 8] &= ~mask
+
+
+def rss_key_window(key, bit):
+    """The 32 key bits starting at @bit, what input bit @bit contributes."""
+    value = 0
+
+    for i in range(32):
+        value = (value << 1) | rss_key_bit(key, bit + i)
+
+    return value
+
+
+def rss_key_toeplitz(key, inp, nbits):
+    """The Toeplitz hash of the @nbits long input @inp under @key."""
+    value = 0
+
+    for i in range(nbits):
+        if rss_key_bit(inp, i):
+            value ^= rss_key_window(key, i)
+
+    return value
+
+
+def rss_key_full_rank(key, lsb, q):
+    """Do the q low order bits of the field at @lsb reach all 2 ** q entries?
+
+    Gaussian elimination over GF(2) on the q windows involved, reduced to
+    their q low order bits, which are the ones indexing the table.
+    """
+    basis = {}
+
+    for j in range(q):
+        vector = rss_key_window(key, lsb - j) & ((1 << q) - 1)
+
+        while vector:
+            low = vector & -vector
+            if low not in basis:
+                basis[low] = vector
+                break
+            vector ^= basis[low]
+
+        if not vector:
+            return False
+
+    return True
+
+
+def rss_key_layout(fields, ipv6):
+    """Describe the hash input built from @fields, as ethtool -n reports it.
+
+    @fields is the flow hash configuration, "sdfn" for a 4-tuple or "sd" for
+    a 2-tuple. Returns the list of (name, position of the least significant
+    bit) and the length of the input in bits, or None if the layout involves
+    something this does not know how to place.
+    """
+    addr_bits = 128 if ipv6 else 32
+    known = (("s", "saddr", addr_bits),
+             ("d", "daddr", addr_bits),
+             ("f", "sport", 16),
+             ("n", "dport", 16))
+
+    if set(fields) - {flag for flag, _, _ in known}:
+        return None, 0
+
+    layout = []
+    nbits = 0
+
+    for flag, name, width in known:
+        if flag not in fields:
+            continue
+        nbits += width
+        layout.append((name, nbits - 1))
+
+    return layout, nbits
diff --git a/tools/testing/selftests/net/netdev_rss_key.py b/tools/testing/selftests/net/netdev_rss_key.py
new file mode 100755
index 0000000000000000000000000000000000000000..37be2ebea6f7c1ece279dbb8c568f7cb06d568cc
--- /dev/null
+++ b/tools/testing/selftests/net/netdev_rss_key.py
@@ -0,0 +1,117 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+
+"""
+Check the quality of the host RSS key, /proc/sys/net/core/netdev_rss_key.
+
+This is the key netdev_rss_key_fill() hands to the drivers. It has to be non
+singular for every field of the hash input and every queue count up to
+2 ** RSS_KEY_QMAX, see lib/py/rsskey.py for what that means and why it
+matters. Unlike the KUnit tests of the generator, this checks the key the
+running kernel has actually handed out.
+
+The key is generated lazily, the first time a driver asks for it, so this
+test skips until a driver has done so. Any NIC calling netdev_rss_key_fill()
+is enough; in a virtual machine, virtio_net does it from
+virtnet_init_default_rss() once RSS has been negotiated.
+
+Checking what a given NIC really uses is a different question, since a
+driver may bring its own key. See drivers/net/hw/rss_key.py for that.
+"""
+
+from lib.py import ksft_run, ksft_exit, ksft_pr
+from lib.py import ksft_eq, ksft_ge
+from lib.py import KsftSkipEx
+from lib.py import RSS_KEY_QMAX, rss_key_full_rank, rss_key_layout
+
+KEY_PATH = "/proc/sys/net/core/netdev_rss_key"
+
+# Shortest key able to hash an IPv6 4-tuple.
+MIN_KEY_LEN = 40
+
+
+def _read_key():
+    """Return the host RSS key, skipping if it has not been generated."""
+    try:
+        with open(KEY_PATH, "r", encoding="ascii") as fp:
+            text = fp.read().strip()
+    except FileNotFoundError as exc:
+        raise KsftSkipEx(f"{KEY_PATH} is not available") from exc
+
+    key = bytes(int(byte, 16) for byte in text.split(":"))
+
+    if not any(key):
+        raise KsftSkipEx("the host RSS key has not been generated yet, "
+                         "no driver has called netdev_rss_key_fill()")
+
+    return key
+
+
+def check_length() -> None:
+    key = _read_key()
+
+    ksft_pr(f"host RSS key is {len(key)} bytes")
+    ksft_ge(len(key), MIN_KEY_LEN, "key too short to hash an IPv6 4-tuple")
+
+
+def check_spread() -> None:
+    key = _read_key()
+    degenerate = []
+
+    for ipv6 in (False, True):
+        layout, _ = rss_key_layout("sdfn", ipv6)
+        family = "IPv6" if ipv6 else "IPv4"
+
+        for name, lsb in layout:
+            if lsb + 32 > len(key) * 8:
+                continue
+
+            for q in range(1, RSS_KEY_QMAX + 1):
+                if not rss_key_full_rank(key, lsb, q):
+                    degenerate.append(f"{family} {name} over {1 << q} queues")
+
+    for bad in degenerate:
+        ksft_pr(f"degenerate: {bad}")
+
+    ksft_eq(len(degenerate), 0,
+            "the host RSS key does not spread flows over all the queues")
+
+
+def check_grid() -> None:
+    """Sweep the whole key, not only the fields of the usual layouts.
+
+    netdev_rss_key_fill() does not get to know what the hardware hashes, so
+    it gives the property at every 16-bit aligned position of the key. A NIC
+    hashing an encapsulated header reads the key well past the first 40
+    bytes, and has to find the same guarantee there.
+    """
+    key = _read_key()
+    bits = len(key) * 8
+    positions = 0
+    degenerate = []
+
+    for lsb in range(15, bits - 31, 16):
+        positions += 1
+
+        for q in range(1, RSS_KEY_QMAX + 1):
+            if not rss_key_full_rank(key, lsb, q):
+                degenerate.append(f"field ending at bit {lsb} "
+                                  f"over {1 << q} queues")
+
+    ksft_pr(f"checked {positions} positions of the {len(key)} byte key")
+
+    for bad in degenerate[:8]:
+        ksft_pr(f"degenerate: {bad}")
+
+    ksft_eq(len(degenerate), 0,
+            "the host RSS key does not spread flows over all the queues "
+            "at every 16-bit aligned position")
+
+
+def main() -> None:
+    ksft_run(globs=globals(), case_pfx={"check_"})
+    ksft_exit()
+
+
+if __name__ == "__main__":
+    main()
-- 
2.55.0.1082.g2b9226bbc0-goog

[PATCH net-next 5/5] selftests: drivers: net: check the RSS key a device uses

From: Eric Dumazet <edumazet@google.com>
Date: 2026-09-21 18:38:08

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

Re: [PATCH net-next 1/5] net: synchronize proc_do_rss_key() with netdev_rss_key_fill()

From: Jakub Kicinski <kuba@kernel.org>
Date: 2026-09-21 19:51:58

On Mon, 21 Sep 2026 18:37:54 +0000 Eric Dumazet wrote:
 	struct ctl_table fake_table;
 	char *pos = buf;
+	bool initialized;
reverse xmas tree

Maybe we should call netdev_rss_key_fill() from the proc handler
and let the net_get_random_once() handle the safe init?

IMHO the concern is entirely academic / hallucinated so adding
LoC for it feels strange

Re: [PATCH net-next 1/5] net: synchronize proc_do_rss_key() with netdev_rss_key_fill()

From: Jakub Kicinski <kuba@kernel.org>
Date: 2026-09-21 19:53:20

On Mon, 21 Sep 2026 12:51:56 -0700 Jakub Kicinski wrote:
Maybe we should call netdev_rss_key_fill() from the proc handler
and let the net_get_random_once() handle the safe init?

IMHO the concern is entirely academic / hallucinated so adding
LoC for it feels strange
I see it's mostly gone in the next commit, I guess that's fine

Re: [PATCH net-next 2/5] net: ethtool: generate RSS keys that spread flows over all queues

From: Jakub Kicinski <kuba@kernel.org>
Date: 2026-09-21 19:59:17

On Mon, 21 Sep 2026 18:37:55 +0000 Eric Dumazet wrote:
 net/ethtool/ioctl.c                  | 172 ++++++++++++++++++++++++++-
nit: perhaps we can move this to common.c or rss.c ?

Re: [PATCH net-next 3/5] net: ethtool: add KUnit tests for the generated RSS key

From: Jakub Kicinski <kuba@kernel.org>
Date: 2026-09-21 20:03:38

On Mon, 21 Sep 2026 18:37:56 +0000 Eric Dumazet wrote:
Check the property from the definition of the Toeplitz hash, independently
of the way netdev_rss_key_init() achieves it: an aligned block of 2^q
consecutive values of one field has to land on the 2^q queues exactly once
each.

Four checks and a control. rss_key_property_test() does the algebra for
the named fields of the usual hash inputs, rss_key_grid_test() sweeps every
16-bit aligned position of the key, since the generator does not get to
know the layout the hardware uses, and rss_key_spread_test() hashes the
inputs of an actual burst and looks at where they land. The control,
rss_key_checker_test(), feeds degenerate keys to the rank check so that a
check accepting everything cannot make the others pass.

rss_key_alias_test() covers the other half of what the generator promises,
that no two input bits read the same 32-bit key window and are therefore
indistinguishable to the hash. It sorts the windows instead of comparing
them pairwise, so unlike netdev_rss_key_init() it looks at every distance
rather than at the multiples of 16 alone.

The field table includes a PSP over UDP over IPv6 layout, whose inner TCP
ports sit far past the plain 4-tuple, because that is the case the offsets
of the standard layouts do not cover.

Commenting out the fixup makes three of the five cases fail and leaves the
control passing. Keeping the fixup but skipping the redraw fails
rss_key_alias_test alone.
Is this AI generated or do you think there's some genuine value here?
I don't want kunits which can be trivially re-generated during
development to be merged. But perhaps there's some genuine value in
this one?

Re: [PATCH net-next 1/5] net: synchronize proc_do_rss_key() with netdev_rss_key_fill()

From: Eric Dumazet <edumazet@google.com>
Date: 2026-09-21 20:04:12

On Mon, Sep 21, 2026 at 9:51 PM Jakub Kicinski [off-list ref] wrote:
On Mon, 21 Sep 2026 18:37:54 +0000 Eric Dumazet wrote:
quoted
      struct ctl_table fake_table;
      char *pos = buf;
+     bool initialized;
reverse xmas tree

Maybe we should call netdev_rss_key_fill() from the proc handler
and let the net_get_random_once() handle the safe init?

IMHO the concern is entirely academic / hallucinated so adding
LoC for it feels strange
Sashiko raised this issue while reviewing the second patch:

Apparently the potential redraws in netdev_rss_key_init() was enough
to flag a bug in netdev_rss_key_init(), without noting this was a
pre-existing one.

Re: [PATCH net-next 5/5] selftests: drivers: net: check the RSS key a device uses

From: Jakub Kicinski <kuba@kernel.org>
Date: 2026-09-21 20:09:42

On Mon, 21 Sep 2026 18:37:58 +0000 Eric Dumazet wrote:
quoted hunk
+++ b/tools/testing/selftests/drivers/net/hw/rss_key.py
Let's fold the tests into one file under drivers/net/ (not hw)
and add to netdevsim the reporting that let's us exercise what we need.

Putting stuff in lib/ should be absolutely last resort.
+    for fl_type in FLOW_TYPES:
+        proc = cmd(f"ethtool -n {cfg.ifname} rx-flow-hash {fl_type}",
+                   fail=False)
netlink supports reporting hash config
+        if proc.ret:
+            continue

Re: [PATCH net-next 3/5] net: ethtool: add KUnit tests for the generated RSS key

From: Eric Dumazet <edumazet@google.com>
Date: 2026-09-21 20:10:40

On Mon, Sep 21, 2026 at 10:03 PM Jakub Kicinski [off-list ref] wrote:
On Mon, 21 Sep 2026 18:37:56 +0000 Eric Dumazet wrote:
quoted
Check the property from the definition of the Toeplitz hash, independently
of the way netdev_rss_key_init() achieves it: an aligned block of 2^q
consecutive values of one field has to land on the 2^q queues exactly once
each.

Four checks and a control. rss_key_property_test() does the algebra for
the named fields of the usual hash inputs, rss_key_grid_test() sweeps every
16-bit aligned position of the key, since the generator does not get to
know the layout the hardware uses, and rss_key_spread_test() hashes the
inputs of an actual burst and looks at where they land. The control,
rss_key_checker_test(), feeds degenerate keys to the rank check so that a
check accepting everything cannot make the others pass.

rss_key_alias_test() covers the other half of what the generator promises,
that no two input bits read the same 32-bit key window and are therefore
indistinguishable to the hash. It sorts the windows instead of comparing
them pairwise, so unlike netdev_rss_key_init() it looks at every distance
rather than at the multiples of 16 alone.

The field table includes a PSP over UDP over IPv6 layout, whose inner TCP
ports sit far past the plain 4-tuple, because that is the case the offsets
of the standard layouts do not cover.

Commenting out the fixup makes three of the five cases fail and leaves the
control passing. Keeping the fixup but skipping the redraw fails
rss_key_alias_test alone.
Is this AI generated or do you think there's some genuine value here?
I don't want kunits which can be trivially re-generated during
development to be merged. But perhaps there's some genuine value in
this one?
I added all these tests because Willem wanted them in our internal bug entry :)

This can certainly be removed, although I have to find ways to launch
vng so that
/proc/sys/net/core/netdev_rss_key is populated.

Re: [PATCH net-next 3/5] net: ethtool: add KUnit tests for the generated RSS key

From: Eric Dumazet <edumazet@google.com>
Date: 2026-09-21 20:35:52

On Mon, Sep 21, 2026 at 10:10 PM Eric Dumazet [off-list ref] wrote:
On Mon, Sep 21, 2026 at 10:03 PM Jakub Kicinski [off-list ref] wrote:
quoted
On Mon, 21 Sep 2026 18:37:56 +0000 Eric Dumazet wrote:
quoted
Check the property from the definition of the Toeplitz hash, independently
of the way netdev_rss_key_init() achieves it: an aligned block of 2^q
consecutive values of one field has to land on the 2^q queues exactly once
each.

Four checks and a control. rss_key_property_test() does the algebra for
the named fields of the usual hash inputs, rss_key_grid_test() sweeps every
16-bit aligned position of the key, since the generator does not get to
know the layout the hardware uses, and rss_key_spread_test() hashes the
inputs of an actual burst and looks at where they land. The control,
rss_key_checker_test(), feeds degenerate keys to the rank check so that a
check accepting everything cannot make the others pass.

rss_key_alias_test() covers the other half of what the generator promises,
that no two input bits read the same 32-bit key window and are therefore
indistinguishable to the hash. It sorts the windows instead of comparing
them pairwise, so unlike netdev_rss_key_init() it looks at every distance
rather than at the multiples of 16 alone.

The field table includes a PSP over UDP over IPv6 layout, whose inner TCP
ports sit far past the plain 4-tuple, because that is the case the offsets
of the standard layouts do not cover.

Commenting out the fixup makes three of the five cases fail and leaves the
control passing. Keeping the fixup but skipping the redraw fails
rss_key_alias_test alone.
Is this AI generated or do you think there's some genuine value here?
I don't want kunits which can be trivially re-generated during
development to be merged. But perhaps there's some genuine value in
this one?
I added all these tests because Willem wanted them in our internal bug entry :)

This can certainly be removed, although I have to find ways to launch
vng so that
/proc/sys/net/core/netdev_rss_key is populated.
Adding the following seems to help:
   --qemu-opts="-netdev user,id=n0 -device
virtio-net-pci,netdev=n0,rss=on,hash=on"

Re: [PATCH net-next 5/5] selftests: drivers: net: check the RSS key a device uses

From: Eric Dumazet <edumazet@google.com>
Date: 2026-09-21 20:47:19

On Mon, Sep 21, 2026 at 10:09 PM Jakub Kicinski [off-list ref] wrote:
On Mon, 21 Sep 2026 18:37:58 +0000 Eric Dumazet wrote:
quoted
+++ b/tools/testing/selftests/drivers/net/hw/rss_key.py
Let's fold the tests into one file under drivers/net/ (not hw)
and add to netdevsim the reporting that let's us exercise what we need.
Ack, will add this in V2.
Putting stuff in lib/ should be absolutely last resort.
quoted
+    for fl_type in FLOW_TYPES:
+        proc = cmd(f"ethtool -n {cfg.ifname} rx-flow-hash {fl_type}",
+                   fail=False)
netlink supports reporting hash config
Ack, thanks!
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help