* [PATCH v13 2/6] net/mlx5e: add TLS 1.3 hardware offload support
From: Rishikesh Jethwani @ 2026-04-29 18:10 UTC (permalink / raw)
To: netdev
Cc: saeedm, tariqt, mbloch, borisp, john.fastabend, kuba, sd, davem,
pabeni, edumazet, leon, Rishikesh Jethwani
In-Reply-To: <20260429181016.3164935-1-rjethwani@purestorage.com>
Enable TLS 1.3 TX/RX hardware offload on ConnectX-6 Dx and newer
crypto-enabled adapters.
Key changes:
- Add TLS 1.3 capability checking and version validation
- Use MLX5E_STATIC_PARAMS_CONTEXT_TLS_1_3 (0x3) for crypto context
- Handle TLS 1.3 IV format: full 12-byte IV copied to gcm_iv +
implicit_iv (vs TLS 1.2's 4-byte salt only)
Tested with TLS 1.3 AES-GCM-128 and AES-GCM-256 cipher suites.
Signed-off-by: Rishikesh Jethwani <rjethwani@purestorage.com>
---
.../ethernet/mellanox/mlx5/core/en_accel/ktls.h | 8 +++++++-
.../mellanox/mlx5/core/en_accel/ktls_txrx.c | 14 +++++++++++---
2 files changed, 18 insertions(+), 4 deletions(-)
diff --git a/drivers/net/ethernet/mellanox/mlx5/core/en_accel/ktls.h b/drivers/net/ethernet/mellanox/mlx5/core/en_accel/ktls.h
index 07a04a142a2e..0469ca6a0762 100644
--- a/drivers/net/ethernet/mellanox/mlx5/core/en_accel/ktls.h
+++ b/drivers/net/ethernet/mellanox/mlx5/core/en_accel/ktls.h
@@ -30,7 +30,9 @@ static inline bool mlx5e_is_ktls_device(struct mlx5_core_dev *mdev)
return false;
return (MLX5_CAP_TLS(mdev, tls_1_2_aes_gcm_128) ||
- MLX5_CAP_TLS(mdev, tls_1_2_aes_gcm_256));
+ MLX5_CAP_TLS(mdev, tls_1_2_aes_gcm_256) ||
+ MLX5_CAP_TLS(mdev, tls_1_3_aes_gcm_128) ||
+ MLX5_CAP_TLS(mdev, tls_1_3_aes_gcm_256));
}
static inline bool mlx5e_ktls_type_check(struct mlx5_core_dev *mdev,
@@ -40,10 +42,14 @@ static inline bool mlx5e_ktls_type_check(struct mlx5_core_dev *mdev,
case TLS_CIPHER_AES_GCM_128:
if (crypto_info->version == TLS_1_2_VERSION)
return MLX5_CAP_TLS(mdev, tls_1_2_aes_gcm_128);
+ else if (crypto_info->version == TLS_1_3_VERSION)
+ return MLX5_CAP_TLS(mdev, tls_1_3_aes_gcm_128);
break;
case TLS_CIPHER_AES_GCM_256:
if (crypto_info->version == TLS_1_2_VERSION)
return MLX5_CAP_TLS(mdev, tls_1_2_aes_gcm_256);
+ else if (crypto_info->version == TLS_1_3_VERSION)
+ return MLX5_CAP_TLS(mdev, tls_1_3_aes_gcm_256);
break;
}
diff --git a/drivers/net/ethernet/mellanox/mlx5/core/en_accel/ktls_txrx.c b/drivers/net/ethernet/mellanox/mlx5/core/en_accel/ktls_txrx.c
index 570a912dd6fa..f3f1be1d4034 100644
--- a/drivers/net/ethernet/mellanox/mlx5/core/en_accel/ktls_txrx.c
+++ b/drivers/net/ethernet/mellanox/mlx5/core/en_accel/ktls_txrx.c
@@ -6,6 +6,7 @@
enum {
MLX5E_STATIC_PARAMS_CONTEXT_TLS_1_2 = 0x2,
+ MLX5E_STATIC_PARAMS_CONTEXT_TLS_1_3 = 0x3,
};
enum {
@@ -15,8 +16,10 @@ enum {
#define EXTRACT_INFO_FIELDS do { \
salt = info->salt; \
rec_seq = info->rec_seq; \
+ iv = info->iv; \
salt_sz = sizeof(info->salt); \
rec_seq_sz = sizeof(info->rec_seq); \
+ iv_sz = sizeof(info->iv); \
} while (0)
static void
@@ -24,9 +27,9 @@ fill_static_params(struct mlx5_wqe_tls_static_params_seg *params,
union mlx5e_crypto_info *crypto_info,
u32 key_id, u32 resync_tcp_sn)
{
+ u16 salt_sz, rec_seq_sz, iv_sz;
+ char *salt, *rec_seq, *iv;
char *initial_rn, *gcm_iv;
- u16 salt_sz, rec_seq_sz;
- char *salt, *rec_seq;
u8 tls_version;
u8 *ctx;
@@ -59,7 +62,12 @@ fill_static_params(struct mlx5_wqe_tls_static_params_seg *params,
memcpy(gcm_iv, salt, salt_sz);
memcpy(initial_rn, rec_seq, rec_seq_sz);
- tls_version = MLX5E_STATIC_PARAMS_CONTEXT_TLS_1_2;
+ if (crypto_info->crypto_info.version == TLS_1_3_VERSION) {
+ memcpy(gcm_iv + salt_sz, iv, iv_sz);
+ tls_version = MLX5E_STATIC_PARAMS_CONTEXT_TLS_1_3;
+ } else {
+ tls_version = MLX5E_STATIC_PARAMS_CONTEXT_TLS_1_2;
+ }
MLX5_SET(tls_static_params, ctx, tls_version, tls_version);
MLX5_SET(tls_static_params, ctx, const_1, 1);
--
2.25.1
^ permalink raw reply related
* [PATCH v13 1/6] net: tls: reject TLS 1.3 offload in chcr_ktls and nfp drivers
From: Rishikesh Jethwani @ 2026-04-29 18:10 UTC (permalink / raw)
To: netdev
Cc: saeedm, tariqt, mbloch, borisp, john.fastabend, kuba, sd, davem,
pabeni, edumazet, leon, Rishikesh Jethwani
In-Reply-To: <20260429181016.3164935-1-rjethwani@purestorage.com>
These drivers only support TLS 1.2. Return early when TLS 1.3
is requested to prevent unsupported hardware offload attempts.
Signed-off-by: Rishikesh Jethwani <rjethwani@purestorage.com>
---
drivers/net/ethernet/chelsio/inline_crypto/ch_ktls/chcr_ktls.c | 3 +++
drivers/net/ethernet/netronome/nfp/crypto/tls.c | 3 +++
2 files changed, 6 insertions(+)
diff --git a/drivers/net/ethernet/chelsio/inline_crypto/ch_ktls/chcr_ktls.c b/drivers/net/ethernet/chelsio/inline_crypto/ch_ktls/chcr_ktls.c
index f5acd4be1e69..29e108ce6764 100644
--- a/drivers/net/ethernet/chelsio/inline_crypto/ch_ktls/chcr_ktls.c
+++ b/drivers/net/ethernet/chelsio/inline_crypto/ch_ktls/chcr_ktls.c
@@ -431,6 +431,9 @@ static int chcr_ktls_dev_add(struct net_device *netdev, struct sock *sk,
atomic64_inc(&port_stats->ktls_tx_connection_open);
u_ctx = adap->uld[CXGB4_ULD_KTLS].handle;
+ if (crypto_info->version != TLS_1_2_VERSION)
+ goto out;
+
if (direction == TLS_OFFLOAD_CTX_DIR_RX) {
pr_err("not expecting for RX direction\n");
goto out;
diff --git a/drivers/net/ethernet/netronome/nfp/crypto/tls.c b/drivers/net/ethernet/netronome/nfp/crypto/tls.c
index 9983d7aa2b9c..13864c6a55dc 100644
--- a/drivers/net/ethernet/netronome/nfp/crypto/tls.c
+++ b/drivers/net/ethernet/netronome/nfp/crypto/tls.c
@@ -287,6 +287,9 @@ nfp_net_tls_add(struct net_device *netdev, struct sock *sk,
BUILD_BUG_ON(offsetof(struct nfp_net_tls_offload_ctx, rx_end) >
TLS_DRIVER_STATE_SIZE_RX);
+ if (crypto_info->version != TLS_1_2_VERSION)
+ return -EOPNOTSUPP;
+
if (!nfp_net_cipher_supported(nn, crypto_info->cipher_type, direction))
return -EOPNOTSUPP;
--
2.25.1
^ permalink raw reply related
* [PATCH net-next v13 0/6] tls: Add TLS 1.3 hardware offload support
From: Rishikesh Jethwani @ 2026-04-29 18:10 UTC (permalink / raw)
To: netdev
Cc: saeedm, tariqt, mbloch, borisp, john.fastabend, kuba, sd, davem,
pabeni, edumazet, leon, Rishikesh Jethwani
Hi all,
This series adds TLS 1.3 hardware offload support including KeyUpdate
(rekey) and a selftest for validation.
Patch 1: Reject TLS 1.3 offload in chcr_ktls and nfp drivers
These drivers only support TLS 1.2; add explicit version check.
Patch 2: mlx5e TLS 1.3 hardware offload
Add TLS 1.3 TX/RX offload on ConnectX-6 Dx and newer.
Handle 12-byte IV format and TLS_1_3 context type.
Patch 3: Core TLS 1.3 hardware offload support
Extend tls_device.c for TLS 1.3 record format (content type
appended before tag). Handle TLS 1.3 IV construction in fallback.
Patch 4: Split tls_set_sw_offload into init/finalize
Allows HW RX path to init SW context, attempt HW setup, then
finalize. Required for proper rekey error handling.
Patch 5: Hardware offload key update (rekey) support
TX: delete old HW context and add new one with updated key. Track
TCP ACKs to flush old-key records before the HW switch; SW path
carries records crossing the rekey boundary.
RX: on peer KeyUpdate, retire the NIC key; queued records that the
NIC already processed under the old key are XOR-undone in software
before AEAD with the new key. NIC re-arming is deferred until the
old-key region has fully drained from the user's recv queue.
Patch 6: Selftest for hardware offload
Python wrapper + C binary using NetDrvEpEnv framework.
Tests TLS 1.2/1.3, AES-GCM-128/256, rekey, various buffer sizes.
Tested on Mellanox ConnectX-6 Dx (Crypto Enabled) with TLS 1.3 AES-GCM-128/256
and multiple rekey cycles.
Rishikesh
Changes in v13:
- RX: on peer KeyUpdate, retire the NIC key; queued records that the
NIC already processed under the old key are XOR-undone in software
before AEAD with the new key. NIC re-arming is deferred until the
old-key region has fully drained from the user's recv queue.
- TX: cancel rekey_sw delayed work in complete_rekey,
EOR-fence the last old-key skb.
- Selftest: new test_tls_offload_burst (TX/RX, RX-ZC) under sustained
rekey.
Rishikesh Jethwani (6):
net: tls: reject TLS 1.3 offload in chcr_ktls and nfp drivers
net/mlx5e: add TLS 1.3 hardware offload support
tls: add TLS 1.3 hardware offload support
tls: split tls_set_sw_offload into init and finalize stages
tls: add hardware offload key update support
selftests: net: add TLS hardware offload test
MAINTAINERS | 2 +
.../chelsio/inline_crypto/ch_ktls/chcr_ktls.c | 3 +
.../mellanox/mlx5/core/en_accel/ktls.h | 8 +-
.../mellanox/mlx5/core/en_accel/ktls_txrx.c | 14 +-
.../net/ethernet/netronome/nfp/crypto/tls.c | 3 +
include/net/tls.h | 84 +-
include/uapi/linux/snmp.h | 2 +
net/tls/tls.h | 33 +-
net/tls/tls_device.c | 815 ++++++++++++++--
net/tls/tls_device_fallback.c | 82 +-
net/tls/tls_main.c | 33 +-
net/tls/tls_proc.c | 2 +
net/tls/tls_sw.c | 153 ++-
net/tls/trace.h | 79 ++
.../selftests/drivers/net/hw/.gitignore | 1 +
.../testing/selftests/drivers/net/hw/Makefile | 2 +
.../selftests/drivers/net/hw/tls_hw_offload.c | 887 ++++++++++++++++++
.../drivers/net/hw/tls_hw_offload.py | 256 +++++
18 files changed, 2271 insertions(+), 188 deletions(-)
create mode 100644 tools/testing/selftests/drivers/net/hw/tls_hw_offload.c
create mode 100755 tools/testing/selftests/drivers/net/hw/tls_hw_offload.py
--
2.25.1
^ permalink raw reply
* Re: [PATCH net-next 3/4] r8152: Add irq mitigation for RTL8157/9
From: Michal Pecio @ 2026-04-29 18:02 UTC (permalink / raw)
To: Birger Koblitz
Cc: Andrew Lunn, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, linux-usb, netdev, linux-kernel,
Chih Kai Hsu
In-Reply-To: <9feb0bc1-b817-46f8-9092-e2beff30ec9d@birger-koblitz.de>
On Wed, 29 Apr 2026 06:06:30 +0200, Birger Koblitz wrote:
> On 29/04/2026 3:56 am, Andrew Lunn wrote:
> > On Tue, Apr 28, 2026 at 05:47:23AM +0200, Birger Koblitz wrote:
> >> Add interrupt mitigation code for both RTL8157 and RTL8159 that prevents
> >> USB interrupt callbacks with urb->status ESHUTDOWN being triggered. While the
> >> issue is rarely seen on the RTL8157, without the mitigation, it is
> >> common on the RTL8159:
> >> [273.561863] r8152 7-1:1.0 enx88c9b3b5xxxx: Stop submitting intr, status -108
> >>
> >> Signed-off-by: Birger Koblitz <mail@birger-koblitz.de>
> >> ---
> >> drivers/net/usb/r8152.c | 6 ++++++
> >> 1 file changed, 6 insertions(+)
> >>
> >> diff --git a/drivers/net/usb/r8152.c b/drivers/net/usb/r8152.c
> >> index 8255261d73148a7b4dabe0188faf07cb1f356437..08cc3c1dae0facb2400890ba4d093c97ed56d40b 100644
> >> --- a/drivers/net/usb/r8152.c
> >> +++ b/drivers/net/usb/r8152.c
> >> @@ -8444,6 +8444,12 @@ static void r8156_init(struct r8152 *tp)
> >> else
> >> r8153_u2p3en(tp, false);
> >>
> >> + if (tp->version >= RTL_VER_16) {
> >> + /* Disable Interrupt Mitigation */
> >> + ocp_byte_clr_bits(tp, MCU_TYPE_USB, 0xcf04,
> >> + BIT(0) | BIT(1) | BIT(2) | BIT(7));
> >> + }
> >
> > What does interrupt mitigation do?
> >
> > Is this a different name for interrupt coalescence, where the MAC
> > delays interrupts for a period of time so more packets are in the
> > receive ring when it does interrupt, so reducing the number of
> > interrupts, and bigger bursts of packets are processed at once?
> >
>
> I do not understand what the mechanism behind this is, there is no
> more documentation in the original driver. I experimented with this
> for some time and the effect that I see is that it prevents
> interrupts after shutdown.
What do you mean by "after shutdown", driver unbind? You shouldn't be
seeing URB completions then if the disconnect() method unlinks them.
And if it doesn't, completions may be using driver data after free.
Or maybe you have pending URBs while calling set_configuration() or
set_interface(), which is dodgy too but at least not asking for panic.
Other cause of ESHUTDOWN might be serious host controller failure, but
you would likely get other log noise with that, at least with xhci.
What shows up if you repro with this enabled?
echo 'module usbcore +p' >/proc/dynamic_debug/control
Regards,
Michal
^ permalink raw reply
* [PATCH net-next] net: usb: cdc_ncm: add Apple Mac USB-C direct networking quirk
From: Alex Cheema @ 2026-04-29 17:57 UTC (permalink / raw)
To: oliver
Cc: bjorn, oleavr, kuba, pabeni, davem, edumazet, andrew+netdev,
netdev, linux-usb, linux-kernel
Apple Silicon Macs expose two CDC NCM "private" data interfaces over
USB-C with VID:PID 0x05ac:0x1905 and product string "Mac". This is the
same protocol Apple already ships on iPhone (0x05ac:0x12a8) and iPad
(0x05ac:0x12ab) for RemoteXPC since iOS 17 -- both data interfaces lack
an interrupt status endpoint, so they rely on the FLAG_LINK_INTR-
conditional bind path introduced in commit 3ec8d7572a69 ("CDC-NCM: add
support for Apple's private interface").
The id_table currently has entries for iPhone and iPad but not for the
Mac. Without a match, cdc_ncm falls through to the generic CDC NCM
class-match entry, which uses the FLAG_LINK_INTR-having cdc_ncm_info
struct, so bind_common() fails on the missing status endpoint and no
netdev appears.
Add id_table entries for both interface numbers (0 and 2) of the Mac,
bound to the existing apple_private_interface_info driver_info.
Verified empirically on a Mac Studio M3 Ultra running macOS 26.5: when
a Mac is connected via USB-C, ioreg shows VID 0x05ac, PID 0x1905,
product string "Mac", with two NCM data interfaces at numbers 0 and 2.
The same PID is presented by all current Apple Silicon Mac models
(MacBook Pro/Air, Mac mini, Mac Studio across the M-series), mirroring
Apple's single-PID-per-family pattern from iPhone/iPad.
After this patch, plugging a Mac into a Linux host running the patched
kernel produces two enx... interfaces (one per data interface),
"ip -br link" lists them as UP, and standard userspace networking
(DHCP, NetworkManager shared mode, etc.) works without any modprobe
overrides or out-of-tree modules.
Signed-off-by: Alex Cheema <alex@exolabs.net>
---
drivers/net/usb/cdc_ncm.c | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/drivers/net/usb/cdc_ncm.c b/drivers/net/usb/cdc_ncm.c
index bb9929727eb9..0223a172851e 100644
--- a/drivers/net/usb/cdc_ncm.c
+++ b/drivers/net/usb/cdc_ncm.c
@@ -2012,6 +2012,14 @@ static const struct usb_device_id cdc_devs[] = {
.driver_info = (unsigned long)&apple_private_interface_info,
},
+ /* Mac */
+ { USB_DEVICE_INTERFACE_NUMBER(0x05ac, 0x1905, 0),
+ .driver_info = (unsigned long)&apple_private_interface_info,
+ },
+ { USB_DEVICE_INTERFACE_NUMBER(0x05ac, 0x1905, 2),
+ .driver_info = (unsigned long)&apple_private_interface_info,
+ },
+
/* Ericsson MBM devices like F5521gw */
{ .match_flags = USB_DEVICE_ID_MATCH_INT_INFO
| USB_DEVICE_ID_MATCH_VENDOR,
--
2.47.1
^ permalink raw reply related
* [PATCH 2/2] netfilter: ip6_tables: allocate hook ops before making table visible
From: Tristan Madani @ 2026-04-29 17:56 UTC (permalink / raw)
To: Pablo Neira Ayuso
Cc: Florian Westphal, Phil Sutter, netfilter-devel, netdev, stable,
linux-kernel, Tristan Madani
In-Reply-To: <20260429175613.1459342-1-tristmd@gmail.com>
From: Tristan Madani <tristan@talencesecurity.com>
ip6t_register_table() first calls xt_register_table() which adds the
table to the per-netns list, making it visible to other code paths. Only
after that does it allocate the per-net copy of hook ops via
kmemdup_array(). This leaves a window where the table is findable via
xt_find_table() but has ops=NULL.
If cleanup_net runs during this window (racing namespace teardown
against lazy table init), ip6t_unregister_table_pre_exit() finds the
table via xt_find_table() and passes the NULL ops pointer to
nf_unregister_net_hooks(), causing a general protection fault when it
dereferences ops[0].pf.
Fix this by allocating the ops array before calling xt_register_table(),
so the table is never visible in the list with a NULL ops pointer.
Fixes: ee177a54413a ("netfilter: ip6_tables: pass table pointer via nf_hook_ops")
Cc: stable@vger.kernel.org
Signed-off-by: Tristan Madani <tristan@talencesecurity.com>
---
net/ipv6/netfilter/ip6_tables.c | 28 ++++++++++++++++------------
1 file changed, 16 insertions(+), 12 deletions(-)
diff --git a/net/ipv6/netfilter/ip6_tables.c b/net/ipv6/netfilter/ip6_tables.c
index d585ac3c11133..17143277637a5 100644
--- a/net/ipv6/netfilter/ip6_tables.c
+++ b/net/ipv6/netfilter/ip6_tables.c
@@ -1754,6 +1754,21 @@ int ip6t_register_table(struct net *net, const struct xt_table *table,
return ret;
}
+ if (template_ops) {
+ num_ops = hweight32(table->valid_hooks);
+ if (num_ops == 0) {
+ xt_free_table_info(newinfo);
+ return -EINVAL;
+ }
+
+ ops = kmemdup_array(template_ops, num_ops, sizeof(*ops),
+ GFP_KERNEL);
+ if (!ops) {
+ xt_free_table_info(newinfo);
+ return -ENOMEM;
+ }
+ }
+
new_table = xt_register_table(net, table, &bootstrap, newinfo);
if (IS_ERR(new_table)) {
struct ip6t_entry *iter;
@@ -1761,24 +1776,13 @@ int ip6t_register_table(struct net *net, const struct xt_table *table,
xt_entry_foreach(iter, loc_cpu_entry, newinfo->size)
cleanup_entry(iter, net);
xt_free_table_info(newinfo);
+ kfree(ops);
return PTR_ERR(new_table);
}
if (!template_ops)
return 0;
- num_ops = hweight32(table->valid_hooks);
- if (num_ops == 0) {
- ret = -EINVAL;
- goto out_free;
- }
-
- ops = kmemdup_array(template_ops, num_ops, sizeof(*ops), GFP_KERNEL);
- if (!ops) {
- ret = -ENOMEM;
- goto out_free;
- }
-
for (i = 0; i < num_ops; i++)
ops[i].priv = new_table;
--
2.47.3
^ permalink raw reply related
* [PATCH 1/2] netfilter: ip_tables: allocate hook ops before making table visible
From: Tristan Madani @ 2026-04-29 17:56 UTC (permalink / raw)
To: Pablo Neira Ayuso
Cc: Florian Westphal, Phil Sutter, netfilter-devel, netdev, stable,
linux-kernel, Tristan Madani
In-Reply-To: <20260429175613.1459342-1-tristmd@gmail.com>
From: Tristan Madani <tristan@talencesecurity.com>
ipt_register_table() adds the table to the per-netns list via
xt_register_table() before allocating the per-netns hook ops copy
via kmemdup_array(). This leaves a window where the table is
visible in the list with ops=NULL.
If cleanup_net() runs during this window (e.g. due to concurrent
netns teardown with failslab-induced allocation failures), the
pre_exit callback finds the table via xt_find_table() and passes
the NULL ops pointer to nf_unregister_net_hooks(), causing a NULL
pointer dereference:
general protection fault in nf_unregister_net_hooks+0xbc/0x150
RIP: nf_unregister_net_hooks (net/netfilter/core.c:613)
Call Trace:
ipt_unregister_table_pre_exit
iptable_mangle_net_pre_exit
ops_pre_exit_list
cleanup_net
Fix by moving the ops allocation before xt_register_table() so
the table is never in the list without valid ops.
Fixes: ae689334225f ("netfilter: ip_tables: pass table pointer via nf_hook_ops")
Cc: stable@vger.kernel.org
Signed-off-by: Tristan Madani <tristan@talencesecurity.com>
---
net/ipv4/netfilter/ip_tables.c | 31 ++++++++++++++++---------------
1 file changed, 16 insertions(+), 15 deletions(-)
diff --git a/net/ipv4/netfilter/ip_tables.c b/net/ipv4/netfilter/ip_tables.c
index 23c8deff8095a..c47bc776eb4f2 100644
--- a/net/ipv4/netfilter/ip_tables.c
+++ b/net/ipv4/netfilter/ip_tables.c
@@ -1745,6 +1745,21 @@ int ipt_register_table(struct net *net, const struct xt_table *table,
return ret;
}
+ if (template_ops) {
+ num_ops = hweight32(table->valid_hooks);
+ if (num_ops == 0) {
+ xt_free_table_info(newinfo);
+ return -EINVAL;
+ }
+
+ ops = kmemdup_array(template_ops, num_ops, sizeof(*ops),
+ GFP_KERNEL);
+ if (!ops) {
+ xt_free_table_info(newinfo);
+ return -ENOMEM;
+ }
+ }
+
new_table = xt_register_table(net, table, &bootstrap, newinfo);
if (IS_ERR(new_table)) {
struct ipt_entry *iter;
@@ -1752,27 +1767,13 @@ int ipt_register_table(struct net *net, const struct xt_table *table,
xt_entry_foreach(iter, loc_cpu_entry, newinfo->size)
cleanup_entry(iter, net);
xt_free_table_info(newinfo);
+ kfree(ops);
return PTR_ERR(new_table);
}
- /* No template? No need to do anything. This is used by 'nat' table, it registers
- * with the nat core instead of the netfilter core.
- */
if (!template_ops)
return 0;
- num_ops = hweight32(table->valid_hooks);
- if (num_ops == 0) {
- ret = -EINVAL;
- goto out_free;
- }
-
- ops = kmemdup_array(template_ops, num_ops, sizeof(*ops), GFP_KERNEL);
- if (!ops) {
- ret = -ENOMEM;
- goto out_free;
- }
-
for (i = 0; i < num_ops; i++)
ops[i].priv = new_table;
--
2.47.3
^ permalink raw reply related
* [PATCH 0/2] netfilter: fix NULL ops race in iptable lazy init
From: Tristan Madani @ 2026-04-29 17:56 UTC (permalink / raw)
To: Pablo Neira Ayuso
Cc: Florian Westphal, Phil Sutter, netfilter-devel, netdev, stable,
linux-kernel, Tristan Madani
From: Tristan Madani <tristan@talencesecurity.com>
ipt_register_table() and ip6t_register_table() call xt_register_table()
which adds the new table to the per-netns list, making it visible to
other code paths. Only afterwards do they allocate the per-net copy of
hook ops via kmemdup_array(). This leaves a window where the table is
findable via xt_find_table() but has ops=NULL.
If cleanup_net runs during this window (racing namespace teardown against
lazy table init), ipt_unregister_table_pre_exit() /
ip6t_unregister_table_pre_exit() finds the table and passes the NULL ops
pointer to nf_unregister_net_hooks(), causing a general protection fault.
Fix both ip_tables.c and ip6_tables.c by moving the ops allocation
before xt_register_table(), so the table is never in the list with a
NULL ops pointer.
Tristan Madani (2):
netfilter: ip_tables: allocate hook ops before making table visible
netfilter: ip6_tables: allocate hook ops before making table visible
net/ipv4/netfilter/ip_tables.c | 31 ++++++++++++++++---------------
net/ipv6/netfilter/ip6_tables.c | 28 ++++++++++++++++------------
2 files changed, 32 insertions(+), 27 deletions(-)
--
2.47.3
^ permalink raw reply
* Re: [PATCH net 0/2] openvswitch: fix self-deadlock on release of tunnel vports
From: Eelco Chaudron @ 2026-04-29 17:46 UTC (permalink / raw)
To: Ilya Maximets
Cc: netdev, Aaron Conole, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Shuah Khan, Yuan Tan,
Yang Yang, dev, linux-kernel, linux-kselftest
In-Reply-To: <20260429151756.4157670-1-i.maximets@ovn.org>
On 29 Apr 2026, at 17:16, Ilya Maximets wrote:
> Two patches - the fix for the actual bug and the selftest that reproduces it.
>
> I missed the self-deadlock in the original patch that introduced the issue,
> because testing required code modification in the ovs-vswitchd to force it to
> use legacy tunnel ports. I thought I made the change correctly, but apparently
> something went wrong and the tests were run with the standard LWT infra instead.
> The selftest added in this patch set will at least prevent this kind of mistakes
> in the future.
>
> I mentioned, however, that these tunnel vports are legacy and not actually used
> by ovs-vswitchd. RTM_NEWLINK + COLLECT_METADATA is used in conjunction with the
> standard OVS_VPORT_TYPE_NETDEV instead since 2017. The code to use the legacy
> tunnels still exists in ovs-vswitchd however, but only as a fallback for older
> kernels and we're planning to remove it in the next release. I'll be sending an
> RFC to remove support for these legacy tunnel types from the kernel, as they
> serve no real purpose today and only increase the uAPI surface for CVEs, but
> we need to fix the known bugs for stable versions.
Thanks, Ilya, for working on this patch! It looks good to me, assuming you
remove the unused socket library in v2.
For the series:
Acked-by: Eelco Chaudron <echaudro@redhat.com>
^ permalink raw reply
* Re: [PATCH RFC net-next v3] hsr: Allow to send a specific port and with HSR header
From: Willem de Bruijn @ 2026-04-29 17:46 UTC (permalink / raw)
To: Sebastian Andrzej Siewior, netdev
Cc: Andrew Lunn, Chintan Vankar, Danish Anwar, Daolin Qiu,
David S. Miller, Eric Dumazet, Felix Maurer, Jakub Kicinski,
Neelima Muralidharan, Paolo Abeni, Praneeth Bajjuri,
Pratheesh Gangadhar TK, Richard Cochran, Simon Horman,
Vignesh Raghavendra, Willem de Bruijn, Sebastian Andrzej Siewior
In-Reply-To: <20260429-hsr_ptp-v3-1-afbf8f200f48@linutronix.de>
Sebastian Andrzej Siewior wrote:
> HSR forwards all packets it received on slave port 1 to slave port 2 and
> one of the two copies to the user (master) interface.
> In terms of PTP this is not good because the latency introduced by
> forwarding makes the timestamp in the PTP packet inaccurate.
> The PTP packets should not forwarded like regular packets.
>
> In order to work with PTP over HSR the following has been done:
> - PTP packets which are received are dropped within the HSR stack. That
> means they are not forwarded or injected into the master port. If the
> user requires them, then they need to be obtained directly from the
> SLAVE interface.
>
> - Sending packets. If the ethernet type of the packet is ETH_P_1588 then
> the stack assumes a header of type struct hsr_inline_header. The size
> of this header is as ethhdr. As a safeguard, the header contains a
> magic field which matches the position of h_source and it needs to
> match HSR_INLINE_HDR.
> Once this is verified, the header contains the port on which this
> packet needs to be sent and if system's HSR header should be added.
> This information is used with the HSR stack and also attached to skb
> as a skb-extension. The packet is then pulled passed the custom header
> so the remaining stack will see the actual data.
> The skb-extension is needed so that an ethernet driver, with
> HSR-offload capabilities, knows that it must not add HSR-header (unless
> requested) and send the packet on both ports.
>
> The originally submitted skb is freed and only the (altered) clone is
> submitted to the slave interface for sending. Therefore on cloning, the
> socket and tx_flags/ tskey are copied so that the PTP timestamp is
> forwarded to the submitting socket.
>
> Signed-off-by: Sebastian Andrzej Siewior <bigeasy@linutronix.de>
Great to see a solution that keeps the added logic mostly within HSR
itself, and works for the userspace component too.
> diff --git a/include/linux/if_hsr.h b/include/linux/if_hsr.h
> index f4cf2dd36d193..220f6e5d7b24c 100644
> --- a/include/linux/if_hsr.h
> +++ b/include/linux/if_hsr.h
> @@ -3,6 +3,7 @@
> #define _LINUX_IF_HSR_H_
>
> #include <linux/types.h>
> +#include <linux/skbuff.h>
>
> struct net_device;
>
> @@ -22,6 +23,21 @@ enum hsr_port_type {
> HSR_PT_PORTS, /* This must be the last item in the enum */
> };
>
> +struct hsr_ptp_ext {
> + u8 port;
> + u8 header;
> +};
> +
> +#define HSR_INLINE_HDR 0xaf485352
> +struct hsr_inline_header {
> + uint8_t tx_port;
> + uint8_t hsr_hdr;
> + uint8_t __pad0[4];
> + uint32_t magic;
> + uint8_t __pad1[2];
> + uint16_t eth_type;
> +} __packed;
> +
No specific need to make this header ethhdr like?
> /* HSR Tag.
> * As defined in IEC-62439-3:2010, the HSR tag is really { ethertype = 0x88FB,
> * path, LSDU_size, sequence Nr }. But we let eth_header() create { h_dest,
> @@ -45,6 +61,60 @@ struct net_device *hsr_get_port_ndev(struct net_device *ndev,
> enum hsr_port_type pt);
> int hsr_get_port_type(struct net_device *hsr_dev, struct net_device *dev,
> enum hsr_port_type *type);
> +
> +static inline bool hsr_skb_has_header(struct sk_buff *skb)
> +{
> + struct hsr_ptp_ext *ptp_ext;
> +
> + ptp_ext = skb_ext_find(skb, SKB_EXT_HSR);
> + if (!ptp_ext)
> + return false;
> + return ptp_ext->header;
> +}
> +
> +static inline unsigned int hsr_skb_has_port(struct sk_buff *skb)
> +{
> + struct hsr_ptp_ext *ptp_ext;
> +
> + if (!skb)
> + return 0;
> +
> + ptp_ext = skb_ext_find(skb, SKB_EXT_HSR);
> + if (!ptp_ext)
> + return 0;
> + return ptp_ext->port;
> +}
> +
> +static inline bool hsr_skb_get_header_port(struct sk_buff *skb, bool *header,
> + enum hsr_port_type *port_type)
> +{
> + struct hsr_ptp_ext *ptp_ext;
> +
> + *port_type = HSR_PT_NONE;
> + *header = false;
> +
> + ptp_ext = skb_ext_find(skb, SKB_EXT_HSR);
> + if (!ptp_ext)
> + return false;
> +
> + *port_type = ptp_ext->port;
> + *header = ptp_ext->header;
> + return true;
> +}
> +
> +static inline bool hsr_skb_add_header_port(struct sk_buff *skb, bool header,
> + enum hsr_port_type port)
> +{
> + struct hsr_ptp_ext *ptp_ext;
> +
> + ptp_ext = skb_ext_add(skb, SKB_EXT_HSR);
> + if (!ptp_ext)
> + return false;
> + ptp_ext->port = port;
> + ptp_ext->header = header;
> + return true;
> +}
> +
> diff --git a/net/hsr/hsr_device.c b/net/hsr/hsr_device.c
> index 5555b71ab19b5..ac39b2347aa0f 100644
> --- a/net/hsr/hsr_device.c
> +++ b/net/hsr/hsr_device.c
> @@ -228,20 +228,51 @@ static netdev_tx_t hsr_dev_xmit(struct sk_buff *skb, struct net_device *dev)
>
> rcu_read_lock();
> master = hsr_port_get_hsr(hsr, HSR_PT_MASTER);
> - if (master) {
> - skb->dev = master->dev;
> - skb_reset_mac_header(skb);
> - skb_reset_mac_len(skb);
> - spin_lock_bh(&hsr->seqnr_lock);
> - hsr_forward_skb(skb, master);
> - spin_unlock_bh(&hsr->seqnr_lock);
> - } else {
> - dev_core_stats_tx_dropped_inc(dev);
> - dev_kfree_skb_any(skb);
> + if (!master)
> + goto drop;
> +
> + skb->dev = master->dev;
> + if (skb->len > ETH_HLEN * 2) {
> + struct hsr_inline_header *hsr_opt;
> +
> + BUILD_BUG_ON(sizeof(struct hsr_inline_header) != sizeof(struct ethhdr));
> + hsr_opt = (struct hsr_inline_header *)skb_mac_header(skb);
> + if (hsr_opt->eth_type == htons(ETH_P_1588) &&
> + hsr_opt->magic == htonl(HSR_INLINE_HDR)) {
> + enum hsr_port_type tx_port;
> + bool has_header;
> +
> + has_header = hsr_opt->hsr_hdr;
> + tx_port = hsr_opt->tx_port;
> + if (tx_port != HSR_PT_SLAVE_A && tx_port != HSR_PT_SLAVE_B)
> + goto drop;
> +
> + if (!hsr_skb_add_header_port(skb, has_header, tx_port))
> + goto drop;
All use of this information happens in the context of this ndo_start_xmit?
If so, no skb_ext needed. Can store the data in skb->cb[], which in
that case is assured to not be overwritten by another layer.
Among various options. skb_ext works, but is a bit heavyhanded for
passing around state within the same layer and call stack.
> +
> + skb_pull(skb, ETH_HLEN);
Prefer sizeof(struct hsr_inline_header)
> + if (has_header)
> + skb_set_network_header(skb, ETH_HLEN + HSR_HLEN);
> + else
> + skb_set_network_header(skb, ETH_HLEN);
> + }
> }
> +
> + skb_reset_mac_header(skb);
> + skb_reset_mac_len(skb);
> + spin_lock_bh(&hsr->seqnr_lock);
> + hsr_forward_skb(skb, master);
> + spin_unlock_bh(&hsr->seqnr_lock);
> +
> rcu_read_unlock();
>
> return NETDEV_TX_OK;
> +
> +drop:
> + dev_core_stats_tx_dropped_inc(dev);
> + dev_kfree_skb_any(skb);
> + rcu_read_unlock();
> + return NETDEV_TX_OK;
> }
>
^ permalink raw reply
* [PATCH net-next v3 5/5] selftests: net: add veth BQL stress test
From: hawk @ 2026-04-29 17:20 UTC (permalink / raw)
To: netdev
Cc: hawk, kernel-team, Jonas Köppeler, Breno Leitao,
David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, Shuah Khan, linux-kernel, linux-kselftest
In-Reply-To: <20260429172036.1028526-1-hawk@kernel.org>
From: Jesper Dangaard Brouer <hawk@kernel.org>
Add a selftest that exercises veth's BQL (Byte Queue Limits) code path
under sustained UDP load. The test creates a veth pair with GRO enabled
(activating the NAPI path and BQL), attaches a qdisc, optionally loads
iptables rules in the consumer namespace to slow NAPI processing, and
floods UDP packets for a configurable duration.
The test serves two purposes: benchmarking BQL's latency impact under
configurable load (iptables rules, qdisc type and parameters), and
detecting kernel BUG/Oops from DQL accounting mismatches. It monitors
dmesg throughout the run and reports PASS/FAIL via kselftest (lib.sh).
Diagnostic output is printed every 5 seconds:
- BQL sysfs inflight/limit and watchdog tx_timeout counter
- qdisc stats: packets, drops, requeues, backlog, qlen, overlimits
- consumer PPS and NAPI-64 cycle time (shows fq_codel target impact)
- sink PPS (per-period delta), latency min/avg/max (stddev at exit)
- ping RTT to measure latency under load
Generating enough traffic to fill the 256-entry ptr_ring requires care:
the UDP sendto() path charges each SKB to sk_wmem_alloc, and the SKB
stays charged (via sock_wfree destructor) until the consumer NAPI thread
finishes processing it -- including any iptables rules in the receive
path. With the default sk_sndbuf (~208KB from wmem_default), only ~93
packets can be in-flight before sendto(MSG_DONTWAIT) returns EAGAIN.
Since 93 < 256 ring entries, the ring never fills and no backpressure
occurs. The test raises wmem_max via sysctl and sets SO_SNDBUF=1MB on
the flood socket to remove this bottleneck. An earlier multi-namespace
routing approach avoided this limit because ip_forward creates new SKBs
detached from the sender's socket.
The --bql-disable option (sets limit_min=1GB) enables A/B comparison.
Typical results with --nrules 6000 --qdisc-opts 'target 2ms interval 20ms':
fq_codel + BQL disabled: ping RTT ~10.8ms, 15% loss, 400KB in ptr_ring
fq_codel + BQL enabled: ping RTT ~0.6ms, 0% loss, 4KB in ptr_ring
Both cases show identical consumer speed (~20Kpps) and fq_codel drops
(~255K), proving the improvement comes purely from where packets buffer.
BQL moves buffering from the ptr_ring into the qdisc, where AQM
(fq_codel/CAKE) can act on it -- eliminating the "dark buffer" that
hides congestion from the scheduler.
The --qdisc-replace mode cycles through sfq/pfifo/fq_codel/noqueue
under active traffic to verify that stale BQL state (STACK_XOFF) is
properly handled during live qdisc transitions.
A companion wrapper (veth_bql_test_virtme.sh) launches the test inside
a virtme-ng VM, with .config validation to prevent silent stalls.
Usage:
sudo ./veth_bql_test.sh [--duration 300] [--nrules 100]
[--qdisc sfq] [--qdisc-opts '...']
[--bql-disable] [--normal-napi]
[--qdisc-replace]
Signed-off-by: Jesper Dangaard Brouer <hawk@kernel.org>
Tested-by: Jonas Köppeler <j.koeppeler@tu-berlin.de>
Tested-by: Breno Leitao <leitao@debian.org>
---
tools/testing/selftests/net/Makefile | 3 +
tools/testing/selftests/net/config | 1 +
tools/testing/selftests/net/napi_poll_hist.bt | 40 +
tools/testing/selftests/net/veth_bql_test.sh | 821 ++++++++++++++++++
.../selftests/net/veth_bql_test_virtme.sh | 124 +++
5 files changed, 989 insertions(+)
create mode 100644 tools/testing/selftests/net/napi_poll_hist.bt
create mode 100755 tools/testing/selftests/net/veth_bql_test.sh
create mode 100755 tools/testing/selftests/net/veth_bql_test_virtme.sh
diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile
index 231245a95879..7f6524169b93 100644
--- a/tools/testing/selftests/net/Makefile
+++ b/tools/testing/selftests/net/Makefile
@@ -119,6 +119,7 @@ TEST_PROGS := \
udpgso_bench.sh \
unicast_extensions.sh \
veth.sh \
+ veth_bql_test.sh \
vlan_bridge_binding.sh \
vlan_hw_filter.sh \
vrf-xfrm-tests.sh \
@@ -196,7 +197,9 @@ TEST_FILES := \
fcnal-test.sh \
in_netns.sh \
lib.sh \
+ napi_poll_hist.bt \
settings \
+ veth_bql_test_virtme.sh \
# end of TEST_FILES
# YNL files, must be before "include ..lib.mk"
diff --git a/tools/testing/selftests/net/config b/tools/testing/selftests/net/config
index 2a390cae41bf..b53cf11232ea 100644
--- a/tools/testing/selftests/net/config
+++ b/tools/testing/selftests/net/config
@@ -101,6 +101,7 @@ CONFIG_NET_SCH_HTB=m
CONFIG_NET_SCH_INGRESS=m
CONFIG_NET_SCH_NETEM=y
CONFIG_NET_SCH_PRIO=m
+CONFIG_NET_SCH_SFQ=m
CONFIG_NET_VRF=y
CONFIG_NF_CONNTRACK=m
CONFIG_NF_CONNTRACK_OVS=y
diff --git a/tools/testing/selftests/net/napi_poll_hist.bt b/tools/testing/selftests/net/napi_poll_hist.bt
new file mode 100644
index 000000000000..34d1a43906bf
--- /dev/null
+++ b/tools/testing/selftests/net/napi_poll_hist.bt
@@ -0,0 +1,40 @@
+#!/usr/bin/env bpftrace
+// SPDX-License-Identifier: GPL-2.0
+// napi_poll work histogram for veth BQL testing.
+// Shows how many packets each NAPI poll processes (0..64).
+// Full-budget (64) polls mean more work is pending; partial (<64) means
+// the ring drained before the budget was exhausted.
+//
+// Usage: bpftrace napi_poll_hist.bt
+// Interval output is a single compact line for easy script parsing.
+
+tracepoint:napi:napi_poll
+/str(args->dev_name, 8) == "veth_bql"/
+{
+ @work = lhist(args->work, 0, 65, 1);
+ @total++;
+ @sum += args->work;
+ if (args->work == args->budget) {
+ @full++;
+ }
+}
+
+interval:s:5
+{
+ $avg = @total > 0 ? @sum / @total : 0;
+ printf("napi_poll: polls=%llu full_budget=%llu partial=%llu avg_work=%llu\n",
+ @total, @full, @total - @full, $avg);
+ clear(@total);
+ clear(@full);
+ clear(@sum);
+}
+
+END
+{
+ printf("\n--- napi_poll work histogram (lifetime) ---\n");
+ print(@work);
+ clear(@work);
+ clear(@total);
+ clear(@full);
+ clear(@sum);
+}
diff --git a/tools/testing/selftests/net/veth_bql_test.sh b/tools/testing/selftests/net/veth_bql_test.sh
new file mode 100755
index 000000000000..bfbbb3432a8f
--- /dev/null
+++ b/tools/testing/selftests/net/veth_bql_test.sh
@@ -0,0 +1,821 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Veth BQL (Byte Queue Limits) stress test and A/B benchmarking tool.
+#
+# Creates a veth pair with GRO on and TSO off (ensures all packets use
+# the NAPI/ptr_ring path where BQL operates), attaches a configurable
+# qdisc, optionally loads iptables rules to slow the consumer NAPI
+# processing, and floods UDP packets at maximum rate.
+#
+# Primary uses:
+# 1) A/B comparison of latency with/without BQL (--bql-disable flag)
+# 2) Testing different qdiscs and their parameters (--qdisc, --qdisc-opts)
+# 3) Detecting kernel BUG/Oops from DQL accounting mismatches
+#
+# Key design detail -- SO_SNDBUF and wmem_max:
+# The UDP sendto() path charges each SKB to the socket's sk_wmem_alloc
+# counter. The SKB carries a destructor (sock_wfree) that releases the
+# charge only after the consumer NAPI thread on the peer veth finishes
+# processing it -- including any iptables rules in the receive path.
+# With the default sk_sndbuf (~208KB from wmem_default), only ~93
+# packets (1442B each) can be in-flight before sendto() returns EAGAIN.
+# Since 93 < 256 ptr_ring entries, the ring never fills and no qdisc
+# backpressure occurs. The test temporarily raises the global wmem_max
+# sysctl and sets SO_SNDBUF=1MB to allow enough in-flight SKBs to
+# saturate the ptr_ring. The original wmem_max is restored on exit.
+#
+# Two TX-stop mechanisms and the dark-buffer problem:
+# DRV_XOFF backpressure (commit dc82a33297fc) stops the TX queue when
+# the 256-entry ptr_ring is full. The queue is released at the end of
+# veth_poll() (commit 5442a9da6978) after processing up to 64 packets
+# (NAPI budget). Without BQL, the entire ring is a FIFO "dark buffer"
+# in front of the qdisc -- packets there are invisible to AQM.
+#
+# BQL adds STACK_XOFF, which dynamically limits in-flight bytes and
+# stops the queue *before* the ring fills. This keeps the ring
+# shallow and moves buffering into the qdisc where sojourn-based AQM
+# (codel, fq_codel, CAKE/COBALT) can measure and drop packets.
+#
+# Sojourn time and NAPI budget interaction:
+# DRV_XOFF releases backpressure once per NAPI poll (up to 64 pkts).
+# During that cycle, packets queued in the qdisc accumulate sojourn
+# time. With fq_codel's default target of 5ms, the threshold is:
+# 5000us / 64 pkts = 78us/pkt --> ~12,800 pps consumer speed.
+# Below that rate the NAPI-64 cycle exceeds the target and fq_codel
+# starts dropping. Use --nrules and --qdisc-opts to experiment.
+#
+cd "$(dirname -- "$0")" || exit 1
+source lib.sh
+
+# Defaults
+DURATION=30 # seconds; use longer --duration to reach DQL counter wrap
+NRULES=3500 # iptables rules in consumer NS (0 to disable)
+QDISC=sfq # qdisc to use (sfq, pfifo, fq_codel, etc.)
+QDISC_OPTS="" # extra qdisc parameters (e.g. "target 1ms interval 10ms")
+BQL_DISABLE=0 # 1 to disable BQL (sets limit_min high)
+NORMAL_NAPI=0 # 1 to use normal softirq NAPI (skip threaded NAPI)
+QDISC_REPLACE=0 # 1 to test qdisc replacement under active traffic
+TINY_FLOOD=0 # 1 to add 2nd UDP thread with min-size packets
+VETH_A="veth_bql0"
+VETH_B="veth_bql1"
+IP_A="10.99.0.1"
+IP_B="10.99.0.2"
+PORT=9999
+PKT_SIZE=1400 # large packets: slower producer, bigger BQL charges
+
+usage() {
+ echo "Usage: $0 [OPTIONS]"
+ echo " --duration SEC test duration (default: $DURATION)"
+ echo " --nrules N iptables rules to slow consumer (default: $NRULES, 0=disable)"
+ echo " --qdisc NAME qdisc to install (default: $QDISC)"
+ echo " --qdisc-opts STR extra qdisc params (e.g. 'target 1ms interval 10ms')"
+ echo " --bql-disable disable BQL for A/B comparison"
+ echo " --normal-napi use softirq NAPI instead of threaded NAPI"
+ echo " --qdisc-replace test qdisc replacement under active traffic"
+ echo " --tiny-flood add 2nd UDP thread with min-size packets (stress BQL bytes)"
+ exit 1
+}
+
+while [ $# -gt 0 ]; do
+ case "$1" in
+ --duration) DURATION="$2"; shift 2 ;;
+ --nrules) NRULES="$2"; shift 2 ;;
+ --qdisc) QDISC="$2"; shift 2 ;;
+ --qdisc-opts) QDISC_OPTS="$2"; shift 2 ;;
+ --bql-disable) BQL_DISABLE=1; shift ;;
+ --normal-napi) NORMAL_NAPI=1; shift ;;
+ --qdisc-replace) QDISC_REPLACE=1; shift ;;
+ --tiny-flood) TINY_FLOOD=1; shift ;;
+ --help|-h) usage ;;
+ *) echo "Unknown option: $1" >&2; usage ;;
+ esac
+done
+
+TMPDIR=$(mktemp -d)
+
+FLOOD_PID=""
+FLOOD2_PID=""
+SINK_PID=""
+PING_PID=""
+BPFTRACE_PID=""
+
+# shellcheck disable=SC2329 # cleanup is invoked indirectly via trap
+cleanup() {
+ [ -n "$BPFTRACE_PID" ] && kill_process "$BPFTRACE_PID"
+ [ -n "$FLOOD_PID" ] && kill_process "$FLOOD_PID"
+ [ -n "$FLOOD2_PID" ] && kill_process "$FLOOD2_PID"
+ [ -n "$SINK_PID" ] && kill_process "$SINK_PID"
+ [ -n "$PING_PID" ] && kill_process "$PING_PID"
+ cleanup_all_ns
+ ip link del "$VETH_A" 2>/dev/null || true
+ [ -n "$ORIG_WMEM_MAX" ] && sysctl -qw net.core.wmem_max="$ORIG_WMEM_MAX"
+ rm -rf "$TMPDIR"
+}
+trap cleanup EXIT
+
+require_command gcc
+require_command ethtool
+require_command tc
+
+# --- Function definitions ---
+
+compile_tools() {
+ echo "--- Compiling UDP flood tool ---"
+cat > "$TMPDIR"/udp_flood.c << 'CEOF'
+#include <arpa/inet.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <time.h>
+#include <unistd.h>
+
+static volatile int running = 1;
+
+static void stop(int sig) { running = 0; }
+
+struct pkt_hdr {
+ struct timespec ts;
+ unsigned long seq;
+};
+
+int main(int argc, char **argv)
+{
+ struct sockaddr_in dst;
+ struct pkt_hdr hdr;
+ unsigned long count = 0;
+ char buf[1500];
+ int sndbuf = 1048576;
+ int pkt_size, max_pkt_size;
+ int cur_size;
+ int duration;
+ int fd;
+
+ if (argc < 5) {
+ fprintf(stderr, "Usage: %s <ip> <pkt_size> <port> <duration> [max_pkt_size]\n",
+ argv[0]);
+ return 1;
+ }
+
+ pkt_size = atoi(argv[2]);
+ if (pkt_size < (int)sizeof(struct pkt_hdr))
+ pkt_size = sizeof(struct pkt_hdr);
+ if (pkt_size > (int)sizeof(buf))
+ pkt_size = sizeof(buf);
+ max_pkt_size = (argc > 5) ? atoi(argv[5]) : pkt_size;
+ if (max_pkt_size < pkt_size)
+ max_pkt_size = pkt_size;
+ if (max_pkt_size > (int)sizeof(buf))
+ max_pkt_size = sizeof(buf);
+ duration = atoi(argv[4]);
+
+ memset(&dst, 0, sizeof(dst));
+ dst.sin_family = AF_INET;
+ dst.sin_port = htons(atoi(argv[3]));
+ inet_pton(AF_INET, argv[1], &dst.sin_addr);
+
+ fd = socket(AF_INET, SOCK_DGRAM, 0);
+ if (fd < 0) {
+ perror("socket");
+ return 1;
+ }
+
+ /* Raise send buffer so sk_wmem_alloc limit doesn't cap
+ * in-flight packets before the ptr_ring (256 entries) fills.
+ * Default wmem_default ~208K only allows ~93 packets.
+ */
+ setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf));
+
+ memset(buf, 0xAA, sizeof(buf));
+ signal(SIGINT, stop);
+ signal(SIGTERM, stop);
+ signal(SIGALRM, stop);
+ alarm(duration);
+
+ while (running) {
+ if (max_pkt_size > pkt_size)
+ cur_size = pkt_size + (rand() % (max_pkt_size - pkt_size + 1));
+ else
+ cur_size = pkt_size;
+ clock_gettime(CLOCK_MONOTONIC, &hdr.ts);
+ hdr.seq = count;
+ memcpy(buf, &hdr, sizeof(hdr));
+ sendto(fd, buf, cur_size, MSG_DONTWAIT,
+ (struct sockaddr *)&dst, sizeof(dst));
+ count++;
+ if (!(count % 10000000))
+ fprintf(stderr, " sent: %lu M packets\n",
+ count / 1000000);
+ }
+
+ fprintf(stderr, "Total sent: %lu packets (%.1f M)\n",
+ count, (double)count / 1e6);
+ close(fd);
+ return 0;
+}
+CEOF
+gcc -O2 -Wall -o "$TMPDIR"/udp_flood "$TMPDIR"/udp_flood.c || exit $ksft_fail
+
+# UDP sink with latency measurement
+cat > "$TMPDIR"/udp_sink.c << 'CEOF'
+#include <arpa/inet.h>
+#include <math.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <time.h>
+#include <unistd.h>
+
+static volatile int running = 1;
+
+static void stop(int sig) { running = 0; }
+
+struct pkt_hdr {
+ struct timespec ts;
+ unsigned long seq;
+};
+
+static void print_periodic(unsigned long count, unsigned long delta_count,
+ double delta_sec, unsigned long drops,
+ unsigned long reorders,
+ double lat_min, double lat_sum,
+ double lat_max)
+{
+ unsigned long pps;
+
+ if (!count)
+ return;
+ pps = delta_sec > 0 ? (unsigned long)(delta_count / delta_sec) : 0;
+ fprintf(stderr, " sink: %lu pkts (%lu pps) drops=%lu reorders=%lu"
+ " latency min/avg/max = %.3f/%.3f/%.3f ms\n",
+ count, pps, drops, reorders,
+ lat_min * 1e3, (lat_sum / count) * 1e3,
+ lat_max * 1e3);
+}
+
+static void print_final(unsigned long count, double elapsed_sec,
+ unsigned long drops, unsigned long reorders,
+ double lat_min, double lat_sum,
+ double lat_sum_sq, double lat_max)
+{
+ unsigned long pps;
+ double avg, stddev;
+
+ if (!count)
+ return;
+ pps = elapsed_sec > 0 ? (unsigned long)(count / elapsed_sec) : 0;
+ avg = lat_sum / count;
+ stddev = sqrt(lat_sum_sq / count - avg * avg);
+ fprintf(stderr, " sink: %lu pkts (%lu avg pps) drops=%lu reorders=%lu"
+ " latency min/avg/max/stddev = %.3f/%.3f/%.3f/%.3f ms\n",
+ count, pps, drops, reorders,
+ lat_min * 1e3, avg * 1e3,
+ lat_max * 1e3, stddev * 1e3);
+}
+
+int main(int argc, char **argv)
+{
+ unsigned long next_seq = 0, drops = 0, reorders = 0;
+ double lat_min = 1e9, lat_max = 0, lat_sum = 0, lat_sum_sq = 0;
+ unsigned long count = 0, last_count = 0;
+ struct sockaddr_in addr;
+ char buf[2048];
+ int fd, one = 1;
+
+ if (argc < 2) {
+ fprintf(stderr, "Usage: %s <port>\n", argv[0]);
+ return 1;
+ }
+
+ fd = socket(AF_INET, SOCK_DGRAM, 0);
+ if (fd < 0) {
+ perror("socket");
+ return 1;
+ }
+ setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
+
+ /* Timeout so recv() unblocks periodically to check 'running' flag.
+ * Needed because glibc signal() sets SA_RESTART, so SIGTERM
+ * does not interrupt recv().
+ */
+ struct timeval tv = { .tv_sec = 1 };
+ setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
+
+ memset(&addr, 0, sizeof(addr));
+ addr.sin_family = AF_INET;
+ addr.sin_port = htons(atoi(argv[1]));
+ addr.sin_addr.s_addr = INADDR_ANY;
+ if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
+ perror("bind");
+ return 1;
+ }
+
+ signal(SIGINT, stop);
+ signal(SIGTERM, stop);
+
+ struct timespec t_start, t_last_print;
+
+ clock_gettime(CLOCK_MONOTONIC, &t_start);
+ t_last_print = t_start;
+
+ while (running) {
+ struct pkt_hdr hdr;
+ struct timespec now;
+ ssize_t n;
+ double lat;
+
+ n = recv(fd, buf, sizeof(buf), 0);
+ if (n < (ssize_t)sizeof(struct pkt_hdr))
+ continue;
+
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ memcpy(&hdr, buf, sizeof(hdr));
+
+ /* Track drops (gaps) and reorders (late arrivals) */
+ if (hdr.seq > next_seq)
+ drops += hdr.seq - next_seq;
+ if (hdr.seq < next_seq)
+ reorders++;
+ if (hdr.seq >= next_seq)
+ next_seq = hdr.seq + 1;
+
+ lat = (now.tv_sec - hdr.ts.tv_sec) +
+ (now.tv_nsec - hdr.ts.tv_nsec) * 1e-9;
+
+ if (lat < lat_min)
+ lat_min = lat;
+ if (lat > lat_max)
+ lat_max = lat;
+ lat_sum += lat;
+ lat_sum_sq += lat * lat;
+ count++;
+
+ {
+ double since_print;
+
+ since_print = (now.tv_sec - t_last_print.tv_sec) +
+ (now.tv_nsec - t_last_print.tv_nsec) * 1e-9;
+ if (since_print >= 5.0) {
+ print_periodic(count, count - last_count,
+ since_print, drops,
+ reorders, lat_min,
+ lat_sum, lat_max);
+ last_count = count;
+ t_last_print = now;
+ }
+ }
+ }
+
+ {
+ struct timespec t_now;
+ double elapsed;
+
+ clock_gettime(CLOCK_MONOTONIC, &t_now);
+ elapsed = (t_now.tv_sec - t_start.tv_sec) +
+ (t_now.tv_nsec - t_start.tv_nsec) * 1e-9;
+ print_final(count, elapsed, drops, reorders,
+ lat_min, lat_sum, lat_sum_sq, lat_max);
+ }
+ close(fd);
+ return 0;
+}
+CEOF
+gcc -O2 -Wall -o "$TMPDIR"/udp_sink "$TMPDIR"/udp_sink.c -lm || exit $ksft_fail
+}
+
+setup_veth() {
+ log_info "Setting up veth pair with GRO"
+ setup_ns NS || exit $ksft_skip
+ ip link add "$VETH_A" type veth peer name "$VETH_B" || \
+ { echo "Failed to create veth pair (need root?)"; exit $ksft_skip; }
+ ip link set "$VETH_B" netns "$NS" || \
+ { echo "Failed to move veth to namespace"; exit $ksft_skip; }
+
+ # Configure IPs
+ ip addr add "${IP_A}/24" dev "$VETH_A"
+ ip link set "$VETH_A" up
+
+ ip -netns "$NS" addr add "${IP_B}/24" dev "$VETH_B"
+ ip -netns "$NS" link set "$VETH_B" up
+
+ # Raise wmem_max so the flood tool's SO_SNDBUF takes effect.
+ # Default 212992 caps in-flight to ~93 packets (sk_wmem_alloc limit),
+ # which is less than the 256-entry ptr_ring and prevents backpressure.
+ ORIG_WMEM_MAX=$(sysctl -n net.core.wmem_max)
+ sysctl -qw net.core.wmem_max=1048576
+
+ # Enable GRO on both ends -- activates NAPI -- BQL code path
+ ethtool -K "$VETH_A" gro on 2>/dev/null || true
+ ip netns exec "$NS" ethtool -K "$VETH_B" gro on 2>/dev/null || true
+
+ # Disable TSO so veth_skb_is_eligible_for_gro() returns true for all
+ # packets, ensuring every SKB takes the NAPI/ptr_ring path. With TSO
+ # enabled, only packets matching sock_wfree + GRO features are eligible;
+ # disabling TSO removes that filter unconditionally.
+ ethtool -K "$VETH_A" tso off gso off 2>/dev/null || true
+ ip netns exec "$NS" ethtool -K "$VETH_B" tso off gso off 2>/dev/null || true
+
+ # Enable threaded NAPI -- this is critical: BQL backpressure (STACK_XOFF)
+ # only engages when producer and consumer run on separate CPUs.
+ # Without threaded NAPI, softirq completions happen too fast for BQL
+ # to build up enough in-flight bytes to trigger the limit.
+ if [ "$NORMAL_NAPI" -eq 0 ]; then
+ echo 1 > /sys/class/net/"$VETH_A"/threaded 2>/dev/null || true
+ ip netns exec "$NS" sh -c "echo 1 > /sys/class/net/$VETH_B/threaded" 2>/dev/null || true
+ log_info "Threaded NAPI enabled"
+ else
+ log_info "Using normal softirq NAPI (threaded NAPI disabled)"
+ fi
+}
+
+install_qdisc() {
+ local qdisc="${1:-$QDISC}"
+ local opts="${2:-}"
+ # Add a qdisc -- veth defaults to noqueue, but BQL needs a qdisc
+ # because STACK_XOFF is checked by the qdisc layer.
+ # Note: qdisc_create() auto-fixes txqueuelen=0 on IFF_NO_QUEUE devices
+ # to DEFAULT_TX_QUEUE_LEN (commit 84c46dd86538).
+ log_info "Installing qdisc: $qdisc $opts"
+ # shellcheck disable=SC2086 # $opts must word-split for tc arguments
+ tc qdisc replace dev "$VETH_A" root $qdisc $opts
+ # shellcheck disable=SC2086
+ ip netns exec "$NS" tc qdisc replace dev "$VETH_B" root $qdisc $opts
+}
+
+remove_qdisc() {
+ log_info "Removing qdisc (reverting to noqueue)"
+ tc qdisc del dev "$VETH_A" root 2>/dev/null || true
+ ip netns exec "$NS" tc qdisc del dev "$VETH_B" root 2>/dev/null || true
+}
+
+setup_iptables() {
+ # Bulk-load iptables rules in consumer namespace to slow NAPI processing.
+ # Many rules force per-packet linear rule traversal, increasing consumer
+ # overhead and BQL inflight bytes -- simulates realistic k8s-like workload.
+ if [ "$NRULES" -gt 0 ]; then
+ # shellcheck disable=SC2016 # single quotes intentional
+ ip netns exec "$NS" bash -c '
+ iptables-restore < <(
+ echo "*filter"
+ for n in $(seq 1 '"$NRULES"'); do
+ echo "-I INPUT -d '"$IP_B"'"
+ done
+ echo "COMMIT"
+ )
+ ' 2>/dev/null || { RET=$ksft_fail retmsg="iptables not available" \
+ log_test "iptables"; exit "$EXIT_STATUS"; }
+ log_info "Loaded $NRULES iptables rules in consumer NS"
+ fi
+}
+
+check_bql_sysfs() {
+ BQL_DIR="/sys/class/net/${VETH_A}/queues/tx-0/byte_queue_limits"
+ if [ -d "$BQL_DIR" ]; then
+ log_info "BQL sysfs found: $BQL_DIR"
+ if [ "$BQL_DISABLE" -eq 1 ]; then
+ echo 1073741824 > "$BQL_DIR/limit_min"
+ log_info "BQL effectively disabled (limit_min=1G)"
+ fi
+ else
+ log_info "BQL sysfs absent (veth IFF_NO_QUEUE+lltx, DQL accounting still active)"
+ BQL_DIR=""
+ fi
+}
+
+start_traffic() {
+ # Snapshot dmesg before test
+ DMESG_BEFORE=$(dmesg | wc -l)
+
+ log_info "Starting UDP sink in namespace"
+ ip netns exec "$NS" "$TMPDIR"/udp_sink "$PORT" &
+ SINK_PID=$!
+ sleep 0.2
+
+ log_info "Starting ping to $IP_B (5/s) to measure latency under load"
+ ping -i 0.2 -w "$DURATION" "$IP_B" > "$TMPDIR"/ping.log 2>&1 &
+ PING_PID=$!
+
+ log_info "Flooding ${PKT_SIZE}-byte UDP packets for ${DURATION}s"
+ "$TMPDIR"/udp_flood "$IP_B" "$PKT_SIZE" "$PORT" "$DURATION" &
+ FLOOD_PID=$!
+
+ # Optional: 2nd UDP thread with tiny packets to stress byte-based BQL.
+ # Small packets charge few BQL bytes, letting many more into the
+ # ptr_ring before STACK_XOFF fires -- exposing the dark buffer.
+ if [ "$TINY_FLOOD" -eq 1 ]; then
+ local port2=$((PORT + 1))
+ ip netns exec "$NS" "$TMPDIR"/udp_sink "$port2" &
+ log_info "Starting 2nd UDP flood (min-size pkts) on port $port2"
+ "$TMPDIR"/udp_flood "$IP_B" 24 "$port2" "$DURATION" &
+ FLOOD2_PID=$!
+ fi
+
+ # Optional: start bpftrace napi_poll histogram (best-effort)
+ local bt_script
+ bt_script="$(dirname -- "$0")/napi_poll_hist.bt"
+ if command -v bpftrace >/dev/null 2>&1 && [ -f "$bt_script" ]; then
+ bpftrace "$bt_script" > "$TMPDIR"/napi_poll.log 2>&1 &
+ BPFTRACE_PID=$!
+ log_info "bpftrace napi_poll histogram started (pid=$BPFTRACE_PID)"
+ fi
+}
+
+stop_traffic() {
+ [ -n "$FLOOD_PID" ] && kill_process "$FLOOD_PID"
+ FLOOD_PID=""
+ [ -n "$FLOOD2_PID" ] && kill_process "$FLOOD2_PID"
+ FLOOD2_PID=""
+ [ -n "$SINK_PID" ] && kill_process "$SINK_PID"
+ SINK_PID=""
+ [ -n "$PING_PID" ] && kill_process "$PING_PID"
+ PING_PID=""
+ [ -n "$BPFTRACE_PID" ] && kill_process "$BPFTRACE_PID"
+ BPFTRACE_PID=""
+}
+
+check_dmesg_bug() {
+ local bug_pattern='kernel BUG|BUG:|Oops:|dql_completed'
+ local warn_pattern='WARNING:|asks to queue packet|NETDEV WATCHDOG'
+ if dmesg | tail -n +$((DMESG_BEFORE + 1)) | \
+ grep -qE "$bug_pattern"; then
+ dmesg | tail -n +$((DMESG_BEFORE + 1)) | \
+ grep -B2 -A20 -E "$bug_pattern|$warn_pattern"
+ return 1
+ fi
+ # Log new warnings since last check (don't repeat old ones)
+ local cur_lines
+ cur_lines=$(dmesg | wc -l)
+ if [ "$cur_lines" -gt "${DMESG_WARN_SEEN:-$DMESG_BEFORE}" ]; then
+ local new_warns
+ new_warns=$(dmesg | tail -n +$(("${DMESG_WARN_SEEN:-$DMESG_BEFORE}" + 1)) | \
+ grep -E "$warn_pattern") || true
+ if [ -n "$new_warns" ]; then
+ local cnt
+ cnt=$(echo "$new_warns" | wc -l)
+ echo " WARN: $cnt new kernel warning(s):"
+ echo "$new_warns" | tail -5
+ fi
+ fi
+ DMESG_WARN_SEEN=$cur_lines
+ return 0
+}
+
+print_periodic_stats() {
+ local elapsed="$1"
+
+ # BQL stats and watchdog counter
+ WD_CNT=$(cat /sys/class/net/${VETH_A}/queues/tx-0/tx_timeout \
+ 2>/dev/null) || WD_CNT="?"
+ if [ -n "$BQL_DIR" ] && [ -d "$BQL_DIR" ]; then
+ INFLIGHT=$(cat "$BQL_DIR/inflight" 2>/dev/null || echo "?")
+ LIMIT=$(cat "$BQL_DIR/limit" 2>/dev/null || echo "?")
+ echo " [${elapsed}s] BQL inflight=${INFLIGHT} limit=${LIMIT}" \
+ "watchdog=${WD_CNT}"
+ else
+ echo " [${elapsed}s] watchdog=${WD_CNT} (no BQL sysfs)"
+ fi
+
+ # Qdisc stats
+ JQ_FMT='"qdisc \(.kind) pkts=\(.packets) drops=\(.drops)'
+ JQ_FMT+=' requeues=\(.requeues) backlog=\(.backlog)'
+ JQ_FMT+=' qlen=\(.qlen) overlimits=\(.overlimits)"'
+ CUR_QPKTS=$(tc -j -s qdisc show dev "$VETH_A" root 2>/dev/null |
+ jq -r '.[0].packets // 0' 2>/dev/null) || CUR_QPKTS=0
+ QSTATS=$(tc -j -s qdisc show dev "$VETH_A" root 2>/dev/null |
+ jq -r ".[0] | $JQ_FMT" 2>/dev/null) &&
+ echo " [${elapsed}s] $QSTATS" || true
+
+ # Consumer PPS and per-packet processing time
+ if [ "$PREV_QPKTS" -gt 0 ] 2>/dev/null; then
+ DELTA=$((CUR_QPKTS - PREV_QPKTS))
+ PPS=$((DELTA / INTERVAL))
+ if [ "$PPS" -gt 0 ]; then
+ PKT_MS=$(awk "BEGIN {printf \"%.3f\", 1000.0/$PPS}")
+ NAPI_MS=$(awk "BEGIN {printf \"%.1f\", 64000.0/$PPS}")
+ echo " [${elapsed}s] consumer: ${PPS} pps" \
+ "(~${PKT_MS}ms/pkt, NAPI-64 cycle ~${NAPI_MS}ms)"
+ fi
+ fi
+ PREV_QPKTS=$CUR_QPKTS
+
+ # softnet_stat: per-CPU tracking to detect same-CPU vs multi-CPU NAPI
+ # /proc/net/softnet_stat columns: processed, dropped, time_squeeze (hex, per-CPU)
+ local cpu=0 total_proc=0 total_sq=0 active_cpus=""
+ while read -r line; do
+ # shellcheck disable=SC2086 # word splitting on $line is intentional
+ set -- $line
+ local cur_p=$((0x${1})) cur_sq=$((0x${3}))
+ if [ -f "$TMPDIR/softnet_cpu${cpu}" ]; then
+ read -r prev_p prev_sq < "$TMPDIR/softnet_cpu${cpu}"
+ local dp=$((cur_p - prev_p)) dsq=$((cur_sq - prev_sq))
+ total_proc=$((total_proc + dp))
+ total_sq=$((total_sq + dsq))
+ [ "$dp" -gt 0 ] && active_cpus="${active_cpus} cpu${cpu}(+${dp})"
+ fi
+ echo "$cur_p $cur_sq" > "$TMPDIR/softnet_cpu${cpu}"
+ cpu=$((cpu + 1))
+ done < /proc/net/softnet_stat
+ local n_active
+ n_active=$(echo "$active_cpus" | wc -w)
+ local cpu_mode="single-CPU"
+ [ "$n_active" -gt 1 ] && cpu_mode="multi-CPU(${n_active})"
+ if [ "$total_sq" -gt 0 ] && [ "$INTERVAL" -gt 0 ]; then
+ echo " [${elapsed}s] softnet: processed=${total_proc}" \
+ "time_squeeze=${total_sq} (${total_sq}/${INTERVAL}s)" \
+ "${cpu_mode}:${active_cpus}"
+ else
+ echo " [${elapsed}s] softnet: processed=${total_proc}" \
+ "time_squeeze=${total_sq}" \
+ "${cpu_mode}:${active_cpus}"
+ fi
+
+ # napi_poll histogram (from bpftrace, if running)
+ if [ -n "$BPFTRACE_PID" ] && [ -f "$TMPDIR"/napi_poll.log ]; then
+ local napi_line
+ napi_line=$(grep '^napi_poll:' "$TMPDIR"/napi_poll.log | tail -1)
+ [ -n "$napi_line" ] && echo " [${elapsed}s] $napi_line"
+ fi
+
+ # Ping RTT
+ PING_RTT=$(tail -1 "$TMPDIR"/ping.log 2>/dev/null | grep -oP 'time=\K[0-9.]+') &&
+ echo " [${elapsed}s] ping RTT=${PING_RTT}ms" || true
+}
+
+monitor_loop() {
+ ELAPSED=0
+ INTERVAL=5
+ PREV_QPKTS=0
+ # Seed per-CPU softnet baselines
+ local cpu=0
+ while read -r line; do
+ # shellcheck disable=SC2086 # word splitting on $line is intentional
+ set -- $line
+ echo "$((0x${1})) $((0x${3}))" > "$TMPDIR/softnet_cpu${cpu}"
+ cpu=$((cpu + 1))
+ done < /proc/net/softnet_stat
+ while kill -0 "$FLOOD_PID" 2>/dev/null; do
+ sleep "$INTERVAL"
+ ELAPSED=$((ELAPSED + INTERVAL))
+
+ if ! check_dmesg_bug; then
+ RET=$ksft_fail
+ retmsg="BUG_ON triggered in dql_completed at ${ELAPSED}s"
+ log_test "veth_bql"
+ exit "$EXIT_STATUS"
+ fi
+
+ print_periodic_stats "$ELAPSED"
+ done
+ wait "$FLOOD_PID" || true
+ FLOOD_PID=""
+}
+
+# Verify traffic is flowing by checking device tx_packets counter.
+# Works for both qdisc and noqueue modes.
+verify_traffic_flowing() {
+ local label="$1"
+ local prev_tx cur_tx
+
+ # Skip check if flood producer already exited (not a stall)
+ if [ -n "$FLOOD_PID" ] && ! kill -0 "$FLOOD_PID" 2>/dev/null; then
+ log_info "$label flood producer exited (duration reached)"
+ return 0
+ fi
+
+ prev_tx=$(cat /sys/class/net/${VETH_A}/statistics/tx_packets \
+ 2>/dev/null) || prev_tx=0
+ sleep 0.5
+ cur_tx=$(cat /sys/class/net/${VETH_A}/statistics/tx_packets \
+ 2>/dev/null) || cur_tx=0
+ if [ "$cur_tx" -gt "$prev_tx" ]; then
+ log_info "$label traffic flowing (tx: $prev_tx -> $cur_tx)"
+ return 0
+ fi
+ log_info "$label traffic STALLED (tx: $prev_tx -> $cur_tx)"
+ return 1
+}
+
+collect_results() {
+ local test_name="${1:-veth_bql}"
+
+ # Ping summary
+ wait "$PING_PID" 2>/dev/null || true
+ PING_PID=""
+ if [ -f "$TMPDIR"/ping.log ]; then
+ PING_LOSS=$(grep -o '[0-9.]*% packet loss' "$TMPDIR"/ping.log) &&
+ log_info "Ping loss: $PING_LOSS"
+ PING_SUMMARY=$(tail -1 "$TMPDIR"/ping.log)
+ log_info "Ping summary: $PING_SUMMARY"
+ fi
+
+ # Watchdog summary
+ WD_FINAL=$(cat /sys/class/net/${VETH_A}/queues/tx-0/tx_timeout \
+ 2>/dev/null) || WD_FINAL=0
+ if [ "$WD_FINAL" -gt 0 ] 2>/dev/null; then
+ log_info "Watchdog fired ${WD_FINAL} time(s)"
+ dmesg | tail -n +$((DMESG_BEFORE + 1)) | \
+ grep -E 'NETDEV WATCHDOG|veth backpressure' || true
+ fi
+
+ # Final dmesg check -- only upgrade to fail, never override existing fail
+ if ! check_dmesg_bug; then
+ RET=$ksft_fail
+ retmsg="BUG_ON triggered in dql_completed"
+ fi
+ log_test "$test_name"
+ exit "$EXIT_STATUS"
+}
+
+# --- Test modes ---
+
+test_bql_stress() {
+ RET=$ksft_pass
+ compile_tools
+ setup_veth
+ install_qdisc "$QDISC" "$QDISC_OPTS"
+ setup_iptables
+ log_info "kernel: $(uname -r)"
+ check_bql_sysfs
+ start_traffic
+ monitor_loop
+ collect_results "veth_bql"
+}
+
+# Test qdisc replacement under active traffic. Cycles through several
+# qdiscs including a transition to noqueue (tc qdisc del) to verify
+# that stale BQL state (STACK_XOFF) is properly reset during qdisc
+# transitions.
+test_qdisc_replace() {
+ local qdiscs=("sfq" "pfifo" "fq_codel")
+ local step=2
+ local elapsed=0
+ local idx
+
+ RET=$ksft_pass
+ compile_tools
+ setup_veth
+ install_qdisc "$QDISC" "$QDISC_OPTS"
+ setup_iptables
+ log_info "kernel: $(uname -r)"
+ check_bql_sysfs
+ start_traffic
+
+ while [ "$elapsed" -lt "$DURATION" ] && kill -0 "$FLOOD_PID" 2>/dev/null; do
+ sleep "$step"
+ elapsed=$((elapsed + step))
+
+ if ! check_dmesg_bug; then
+ RET=$ksft_fail
+ retmsg="BUG_ON during qdisc replacement at ${elapsed}s"
+ break
+ fi
+
+ # Cycle: sfq -> pfifo -> fq_codel -> noqueue -> sfq -> ...
+ idx=$(( (elapsed / step - 1) % (${#qdiscs[@]} + 1) ))
+ if [ "$idx" -eq "${#qdiscs[@]}" ]; then
+ remove_qdisc
+ else
+ install_qdisc "${qdiscs[$idx]}"
+ fi
+
+ # Print BQL and qdisc stats after each replacement
+ if [ -n "$BQL_DIR" ] && [ -d "$BQL_DIR" ]; then
+ local inflight limit limit_min limit_max holding
+ inflight=$(cat "$BQL_DIR/inflight" 2>/dev/null || echo "?")
+ limit=$(cat "$BQL_DIR/limit" 2>/dev/null || echo "?")
+ limit_min=$(cat "$BQL_DIR/limit_min" 2>/dev/null || echo "?")
+ limit_max=$(cat "$BQL_DIR/limit_max" 2>/dev/null || echo "?")
+ holding=$(cat "$BQL_DIR/holding_time" 2>/dev/null || echo "?")
+ echo " [${elapsed}s] BQL inflight=${inflight} limit=${limit}" \
+ "limit_min=${limit_min} limit_max=${limit_max}" \
+ "holding=${holding}"
+ fi
+ local cur_qdisc
+ cur_qdisc=$(tc qdisc show dev "$VETH_A" root 2>/dev/null | \
+ awk '{print $2}') || cur_qdisc="none"
+ local txq_state
+ txq_state=$(cat /sys/class/net/${VETH_A}/queues/tx-0/tx_timeout \
+ 2>/dev/null) || txq_state="?"
+ echo " [${elapsed}s] qdisc=${cur_qdisc} watchdog=${txq_state}"
+
+ if ! verify_traffic_flowing "[${elapsed}s]"; then
+ RET=$ksft_fail
+ retmsg="Traffic stalled after qdisc replacement at ${elapsed}s"
+ break
+ fi
+ done
+
+ stop_traffic
+ collect_results "veth_bql_qdisc_replace"
+}
+
+# --- Main ---
+if [ "$QDISC_REPLACE" -eq 1 ]; then
+ test_qdisc_replace
+else
+ test_bql_stress
+fi
diff --git a/tools/testing/selftests/net/veth_bql_test_virtme.sh b/tools/testing/selftests/net/veth_bql_test_virtme.sh
new file mode 100755
index 000000000000..bb8dde0f6c00
--- /dev/null
+++ b/tools/testing/selftests/net/veth_bql_test_virtme.sh
@@ -0,0 +1,124 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Launch veth BQL test inside virtme-ng
+#
+# Must be run from the kernel build tree root.
+#
+# Options:
+# --verbose Show kernel console (vng boot messages) in real time.
+# Useful for debugging kernel panics / BUG_ON crashes.
+# All other options are forwarded to veth_bql_test.sh (see --help there).
+#
+# Examples (run from kernel tree root):
+# ./tools/testing/selftests/net/veth_bql_test_virtme.sh [OPTIONS]
+# --duration 20 --nrules 1000
+# --qdisc fq_codel --bql-disable
+# --verbose --qdisc-replace --duration 60
+
+set -eu
+
+# Parse --verbose (consumed here, not forwarded to the inner test).
+VERBOSE=""
+INNER_ARGS=()
+for arg in "$@"; do
+ if [ "$arg" = "--verbose" ]; then
+ VERBOSE="--verbose"
+ else
+ INNER_ARGS+=("$arg")
+ fi
+done
+TEST_ARGS=""
+[ ${#INNER_ARGS[@]} -gt 0 ] && TEST_ARGS=$(printf '%q ' "${INNER_ARGS[@]}")
+
+if [ ! -f "vmlinux" ]; then
+ echo "ERROR: virtme-ng needs vmlinux; run from a compiled kernel tree:" >&2
+ echo " cd /path/to/kernel && $0" >&2
+ exit 1
+fi
+
+# Verify .config has the options needed for virtme-ng and this test.
+# Without these the VM silently stalls with no output.
+KCONFIG=".config"
+if [ ! -f "$KCONFIG" ]; then
+ echo "ERROR: No .config found -- build the kernel first" >&2
+ exit 1
+fi
+
+MISSING=""
+for opt in CONFIG_VIRTIO CONFIG_VIRTIO_PCI CONFIG_VIRTIO_NET \
+ CONFIG_VIRTIO_CONSOLE CONFIG_NET_9P CONFIG_NET_9P_VIRTIO \
+ CONFIG_9P_FS CONFIG_VETH CONFIG_BQL; do
+ if ! grep -q "^${opt}=[ym]" "$KCONFIG"; then
+ MISSING+=" $opt\n"
+ fi
+done
+if [ -n "$MISSING" ]; then
+ echo "ERROR: .config is missing options required by virtme-ng:" >&2
+ echo -e "$MISSING" >&2
+ echo "Consider: vng --kconfig (or make defconfig + enable above)" >&2
+ exit 1
+fi
+
+TESTDIR="tools/testing/selftests/net"
+TESTNAME="veth_bql_test.sh"
+LOGFILE="veth_bql_test.log"
+LOGPATH="$TESTDIR/$LOGFILE"
+CONSOLELOG="veth_bql_console.log"
+rm -f "$LOGPATH" "$CONSOLELOG"
+
+echo "Starting VM... test output in $LOGPATH, kernel console in $CONSOLELOG"
+echo "(VM is booting, please wait ~30s)"
+
+# Always capture kernel console to a file via a second QEMU serial port.
+# vng claims ttyS0 (mapped to /dev/null); --qemu-opts adds ttyS1 on COM2.
+# earlycon registers COM2's I/O port (0x2f8) as a persistent console.
+# (plain console=ttyS1 does NOT work: the 8250 driver registers once,
+# ttyS0 wins, and ttyS1 is never picked up.)
+# --verbose additionally shows kernel console in real time on the terminal.
+SERIAL_CONSOLE="earlycon=uart8250,io,0x2f8,115200"
+SERIAL_CONSOLE+=" console=uart8250,io,0x2f8,115200"
+set +e
+vng $VERBOSE --cpus 4 --memory 2G \
+ --rwdir "$TESTDIR" \
+ --append "panic=5 loglevel=4 $SERIAL_CONSOLE" \
+ --qemu-opts="-serial file:$CONSOLELOG" \
+ --exec "cd $TESTDIR && \
+ ./$TESTNAME $TEST_ARGS 2>&1 | \
+ tee $LOGFILE; echo EXIT_CODE=\$? >> $LOGFILE"
+VNG_RC=$?
+set -e
+
+echo ""
+if [ "$VNG_RC" -ne 0 ]; then
+ echo "***********************************************************"
+ echo "* VM CRASHED -- kernel panic or BUG_ON (vng rc=$VNG_RC)"
+ echo "***********************************************************"
+ if [ -s "$CONSOLELOG" ] && \
+ grep -qiE 'kernel BUG|BUG:|Oops:|panic|dql_completed' "$CONSOLELOG"; then
+ echo ""
+ echo "--- kernel backtrace ($CONSOLELOG) ---"
+ grep -iE -A30 'kernel BUG|BUG:|Oops:|panic|dql_completed' \
+ "$CONSOLELOG" | head -50
+ else
+ echo ""
+ echo "Re-run with --verbose to see the kernel backtrace:"
+ echo " $0 --verbose ${INNER_ARGS[*]}"
+ fi
+ exit 1
+elif [ ! -f "$LOGPATH" ]; then
+ echo "No log file found -- VM may have crashed before writing output"
+ exit 2
+else
+ echo "=== VM finished ==="
+fi
+
+# Scan console log for unexpected kernel warnings (even on clean exit)
+if [ -s "$CONSOLELOG" ]; then
+ WARN_PATTERN='kernel BUG|BUG:|Oops:|dql_completed|WARNING:|asks to queue packet|NETDEV WATCHDOG'
+ WARN_LINES=$(grep -cE "$WARN_PATTERN" "$CONSOLELOG" 2>/dev/null) || WARN_LINES=0
+ if [ "$WARN_LINES" -gt 0 ]; then
+ echo ""
+ echo "*** kernel warnings in $CONSOLELOG ($WARN_LINES lines) ***"
+ grep -E "$WARN_PATTERN" "$CONSOLELOG" | head -20
+ fi
+fi
--
2.43.0
^ permalink raw reply related
* [PATCH net-next v3 4/5] net: sched: add timeout count to NETDEV WATCHDOG message
From: hawk @ 2026-04-29 17:20 UTC (permalink / raw)
To: netdev
Cc: hawk, kernel-team, Jakub Kicinski, Jonas Köppeler,
Jamal Hadi Salim, Jiri Pirko, David S. Miller, Eric Dumazet,
Paolo Abeni, Simon Horman, linux-kernel
In-Reply-To: <20260429172036.1028526-1-hawk@kernel.org>
From: Jesper Dangaard Brouer <hawk@kernel.org>
Add the per-queue timeout counter (trans_timeout) to the core NETDEV
WATCHDOG log message. This makes it easy to determine how frequently
a particular queue is stalling from a single log line, without having
to search through and correlate spaced-out log entries.
Useful for production monitoring where timeouts are spaced by the
watchdog interval, making frequency hard to judge.
Suggested-by: Jakub Kicinski <kuba@kernel.org>
Link: https://lore.kernel.org/all/20251107175445.58eba452@kernel.org/
Signed-off-by: Jesper Dangaard Brouer <hawk@kernel.org>
Tested-by: Jonas Köppeler <j.koeppeler@tu-berlin.de>
---
net/sched/sch_generic.c | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/net/sched/sch_generic.c b/net/sched/sch_generic.c
index a93321db8fd7..3e2e2e887a86 100644
--- a/net/sched/sch_generic.c
+++ b/net/sched/sch_generic.c
@@ -533,13 +533,12 @@ static void dev_watchdog(struct timer_list *t)
netif_running(dev) &&
netif_carrier_ok(dev)) {
unsigned int timedout_ms = 0;
+ struct netdev_queue *txq;
unsigned int i;
unsigned long trans_start;
unsigned long oldest_start = jiffies;
for (i = 0; i < dev->num_tx_queues; i++) {
- struct netdev_queue *txq;
-
txq = netdev_get_tx_queue(dev, i);
if (!netif_xmit_stopped(txq))
continue;
@@ -561,9 +560,10 @@ static void dev_watchdog(struct timer_list *t)
if (unlikely(timedout_ms)) {
trace_net_dev_xmit_timeout(dev, i);
- netdev_crit(dev, "NETDEV WATCHDOG: CPU: %d: transmit queue %u timed out %u ms\n",
+ netdev_crit(dev, "NETDEV WATCHDOG: CPU: %d: transmit queue %u timed out %u ms (n:%ld)\n",
raw_smp_processor_id(),
- i, timedout_ms);
+ i, timedout_ms,
+ atomic_long_read(&txq->trans_timeout));
netif_freeze_queues(dev);
dev->netdev_ops->ndo_tx_timeout(dev, i);
netif_unfreeze_queues(dev);
--
2.43.0
^ permalink raw reply related
* [PATCH net-next v3 3/5] veth: add tx_timeout watchdog as BQL safety net
From: hawk @ 2026-04-29 17:20 UTC (permalink / raw)
To: netdev
Cc: hawk, kernel-team, Jonas Köppeler, Andrew Lunn,
David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
linux-kernel
In-Reply-To: <20260429172036.1028526-1-hawk@kernel.org>
From: Jesper Dangaard Brouer <hawk@kernel.org>
With the introduction of BQL (Byte Queue Limits) for veth, there are
now two independent mechanisms that can stop a transmit queue:
- DRV_XOFF: set by netif_tx_stop_queue() when the ptr_ring is full
- STACK_XOFF: set by BQL when the byte-in-flight limit is reached
If either mechanism stalls without a corresponding wake/completion,
the queue stops permanently. Enable the net device watchdog timer and
implement ndo_tx_timeout as a failsafe recovery.
The timeout handler resets BQL state (clearing STACK_XOFF) and wakes
the queue (clearing DRV_XOFF), covering both stop mechanisms. The
watchdog fires after 16 seconds, which accommodates worst-case NAPI
processing (budget=64 packets x 250ms per-packet consumer delay)
without false positives under normal backpressure.
Signed-off-by: Jesper Dangaard Brouer <hawk@kernel.org>
Tested-by: Jonas Köppeler <j.koeppeler@tu-berlin.de>
---
drivers/net/veth.c | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/drivers/net/veth.c b/drivers/net/veth.c
index 3de25ba34a90..9d7b085c9548 100644
--- a/drivers/net/veth.c
+++ b/drivers/net/veth.c
@@ -1431,6 +1431,22 @@ static int veth_set_channels(struct net_device *dev,
goto out;
}
+static void veth_tx_timeout(struct net_device *dev, unsigned int txqueue)
+{
+ struct netdev_queue *txq = netdev_get_tx_queue(dev, txqueue);
+
+ netdev_err(dev,
+ "veth backpressure(0x%lX) stalled(n:%ld) TXQ(%u) re-enable\n",
+ txq->state, atomic_long_read(&txq->trans_timeout), txqueue);
+
+ /* Cannot call netdev_tx_reset_queue(): dql_reset() races with
+ * peer NAPI calling dql_completed() concurrently.
+ * Just clear the stop bits; the qdisc will re-stop if still stuck.
+ */
+ clear_bit(__QUEUE_STATE_STACK_XOFF, &txq->state);
+ netif_tx_wake_queue(txq);
+}
+
static int veth_open(struct net_device *dev)
{
struct veth_priv *priv = netdev_priv(dev);
@@ -1769,6 +1785,7 @@ static const struct net_device_ops veth_netdev_ops = {
.ndo_bpf = veth_xdp,
.ndo_xdp_xmit = veth_ndo_xdp_xmit,
.ndo_get_peer_dev = veth_peer_dev,
+ .ndo_tx_timeout = veth_tx_timeout,
};
static const struct xdp_metadata_ops veth_xdp_metadata_ops = {
@@ -1808,6 +1825,7 @@ static void veth_setup(struct net_device *dev)
dev->priv_destructor = veth_dev_free;
dev->pcpu_stat_type = NETDEV_PCPU_STAT_TSTATS;
dev->max_mtu = ETH_MAX_MTU;
+ dev->watchdog_timeo = msecs_to_jiffies(16000);
dev->hw_features = VETH_FEATURES;
dev->hw_enc_features = VETH_FEATURES;
--
2.43.0
^ permalink raw reply related
* [PATCH net-next v3 2/5] veth: implement Byte Queue Limits (BQL) for latency reduction
From: hawk @ 2026-04-29 17:20 UTC (permalink / raw)
To: netdev
Cc: hawk, kernel-team, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Alexei Starovoitov, Daniel Borkmann,
John Fastabend, Stanislav Fomichev, linux-kernel, bpf
In-Reply-To: <20260429172036.1028526-1-hawk@kernel.org>
From: Jesper Dangaard Brouer <hawk@kernel.org>
Commit dc82a33297fc ("veth: apply qdisc backpressure on full ptr_ring to
reduce TX drops") gave qdiscs control over veth by returning
NETDEV_TX_BUSY when the ptr_ring is full (DRV_XOFF). That commit noted
a known limitation: the 256-entry ptr_ring sits in front of the qdisc as
a dark buffer, adding base latency because the qdisc has no visibility
into how many bytes are already queued there.
Add BQL support so the qdisc gets feedback and can begin shaping traffic
before the ring fills. In testing with fq_codel, BQL reduces ping RTT
under UDP load from ~6.61ms to ~0.36ms (18x).
Charge a fixed VETH_BQL_UNIT (1) per packet rather than skb->len, so
the DQL limit tracks packets-in-flight. Unlike a physical NIC, veth
has no link speed -- the ptr_ring drains at CPU speed and is
packet-indexed, not byte-indexed, so bytes are not the natural unit.
With byte-based charging, small packets sneak many more entries into
the ring before STACK_XOFF fires, deepening the dark buffer under
mixed-size workloads. Testing with a concurrent min-size packet flood
shows 3.7x ping RTT degradation with skb->len charging versus no
change with fixed-unit charging.
Charge BQL inside veth_xdp_rx() under the ptr_ring producer_lock, after
confirming the ring is not full. The charge must precede the produce
because the NAPI consumer can run on another CPU and complete the SKB
the instant it becomes visible in the ring. Doing both under the same
lock avoids a pre-charge/undo pattern -- BQL is only charged when
produce is guaranteed to succeed.
BQL is enabled only when a real qdisc is attached (guarded by
!qdisc_txq_has_no_queue), as HARD_TX_LOCK provides serialization
for TXQ modification like dql_queued(). For lltx devices, like veth,
this HARD_TX_LOCK serialization isn't provided. The ptr_ring
producer_lock provides additional serialization that would allow
BQL to work correctly even with noqueue, though that combination
is not currently enabled, as the netstack will drop and warn.
Track per-SKB BQL state via a VETH_BQL_FLAG pointer tag in the ptr_ring
entry. This is necessary because the qdisc can be replaced live while
SKBs are in-flight -- each SKB must carry the charge decision made at
enqueue time rather than re-checking the peer's qdisc at completion.
Complete per-SKB in veth_xdp_rcv() rather than in bulk, so STACK_XOFF
clears promptly when producer and consumer run on different CPUs.
BQL introduces a second independent queue-stop mechanism (STACK_XOFF)
alongside the existing DRV_XOFF (ring full). Both must be clear for
the queue to transmit. Reset BQL state in veth_napi_del_range() after
synchronize_net() to avoid racing with in-flight veth_poll() calls.
Clamp the reset loop to the peer's real_num_tx_queues, since the peer
may have fewer TX queues than the local device has RX queues (e.g. when
veth is enslaved to a bond with XDP attached).
Signed-off-by: Jesper Dangaard Brouer <hawk@kernel.org>
---
drivers/net/veth.c | 76 +++++++++++++++++++++++++++++++++++++++-------
1 file changed, 65 insertions(+), 11 deletions(-)
diff --git a/drivers/net/veth.c b/drivers/net/veth.c
index e35df717e65e..3de25ba34a90 100644
--- a/drivers/net/veth.c
+++ b/drivers/net/veth.c
@@ -34,9 +34,13 @@
#define DRV_VERSION "1.0"
#define VETH_XDP_FLAG BIT(0)
+#define VETH_BQL_FLAG BIT(1)
#define VETH_RING_SIZE 256
#define VETH_XDP_HEADROOM (XDP_PACKET_HEADROOM + NET_IP_ALIGN)
+/* Fixed BQL charge: DQL limit tracks packets-in-flight, not bytes */
+#define VETH_BQL_UNIT 1
+
#define VETH_XDP_TX_BULK_SIZE 16
#define VETH_XDP_BATCH 16
@@ -280,6 +284,21 @@ static bool veth_is_xdp_frame(void *ptr)
return (unsigned long)ptr & VETH_XDP_FLAG;
}
+static bool veth_ptr_is_bql(void *ptr)
+{
+ return (unsigned long)ptr & VETH_BQL_FLAG;
+}
+
+static struct sk_buff *veth_ptr_to_skb(void *ptr)
+{
+ return (void *)((unsigned long)ptr & ~VETH_BQL_FLAG);
+}
+
+static void *veth_skb_to_ptr(struct sk_buff *skb, bool bql)
+{
+ return bql ? (void *)((unsigned long)skb | VETH_BQL_FLAG) : skb;
+}
+
static struct xdp_frame *veth_ptr_to_xdp(void *ptr)
{
return (void *)((unsigned long)ptr & ~VETH_XDP_FLAG);
@@ -295,7 +314,7 @@ static void veth_ptr_free(void *ptr)
if (veth_is_xdp_frame(ptr))
xdp_return_frame(veth_ptr_to_xdp(ptr));
else
- kfree_skb(ptr);
+ kfree_skb(veth_ptr_to_skb(ptr));
}
static void __veth_xdp_flush(struct veth_rq *rq)
@@ -309,19 +328,33 @@ static void __veth_xdp_flush(struct veth_rq *rq)
}
}
-static int veth_xdp_rx(struct veth_rq *rq, struct sk_buff *skb)
+static int veth_xdp_rx(struct veth_rq *rq, struct sk_buff *skb, bool do_bql,
+ struct netdev_queue *txq)
{
- if (unlikely(ptr_ring_produce(&rq->xdp_ring, skb)))
+ struct ptr_ring *ring = &rq->xdp_ring;
+
+ spin_lock(&ring->producer_lock);
+ if (unlikely(!ring->size) || __ptr_ring_full(ring)) {
+ spin_unlock(&ring->producer_lock);
return NETDEV_TX_BUSY; /* signal qdisc layer */
+ }
+
+ /* BQL charge before produce; consumer cannot see entry yet */
+ if (do_bql)
+ netdev_tx_sent_queue(txq, VETH_BQL_UNIT);
+
+ __ptr_ring_produce(ring, veth_skb_to_ptr(skb, do_bql));
+ spin_unlock(&ring->producer_lock);
return NET_RX_SUCCESS; /* same as NETDEV_TX_OK */
}
static int veth_forward_skb(struct net_device *dev, struct sk_buff *skb,
- struct veth_rq *rq, bool xdp)
+ struct veth_rq *rq, bool xdp, bool do_bql,
+ struct netdev_queue *txq)
{
return __dev_forward_skb(dev, skb) ?: xdp ?
- veth_xdp_rx(rq, skb) :
+ veth_xdp_rx(rq, skb, do_bql, txq) :
__netif_rx(skb);
}
@@ -352,6 +385,7 @@ static netdev_tx_t veth_xmit(struct sk_buff *skb, struct net_device *dev)
struct net_device *rcv;
int length = skb->len;
bool use_napi = false;
+ bool do_bql = false;
int ret, rxq;
rcu_read_lock();
@@ -375,8 +409,11 @@ static netdev_tx_t veth_xmit(struct sk_buff *skb, struct net_device *dev)
}
skb_tx_timestamp(skb);
+ txq = netdev_get_tx_queue(dev, rxq);
- ret = veth_forward_skb(rcv, skb, rq, use_napi);
+ /* BQL charge happens inside veth_xdp_rx() under producer_lock */
+ do_bql = use_napi && !qdisc_txq_has_no_queue(txq);
+ ret = veth_forward_skb(rcv, skb, rq, use_napi, do_bql, txq);
switch (ret) {
case NET_RX_SUCCESS: /* same as NETDEV_TX_OK */
if (!use_napi)
@@ -388,8 +425,6 @@ static netdev_tx_t veth_xmit(struct sk_buff *skb, struct net_device *dev)
/* If a qdisc is attached to our virtual device, returning
* NETDEV_TX_BUSY is allowed.
*/
- txq = netdev_get_tx_queue(dev, rxq);
-
if (qdisc_txq_has_no_queue(txq)) {
dev_kfree_skb_any(skb);
goto drop;
@@ -412,6 +447,7 @@ static netdev_tx_t veth_xmit(struct sk_buff *skb, struct net_device *dev)
net_crit_ratelimited("%s(%s): Invalid return code(%d)",
__func__, dev->name, ret);
}
+
rcu_read_unlock();
return ret;
@@ -900,7 +936,8 @@ static struct sk_buff *veth_xdp_rcv_skb(struct veth_rq *rq,
static int veth_xdp_rcv(struct veth_rq *rq, int budget,
struct veth_xdp_tx_bq *bq,
- struct veth_stats *stats)
+ struct veth_stats *stats,
+ struct netdev_queue *peer_txq)
{
int i, done = 0, n_xdpf = 0;
void *xdpf[VETH_XDP_BATCH];
@@ -928,9 +965,13 @@ static int veth_xdp_rcv(struct veth_rq *rq, int budget,
}
} else {
/* ndo_start_xmit */
- struct sk_buff *skb = ptr;
+ bool bql_charged = veth_ptr_is_bql(ptr);
+ struct sk_buff *skb = veth_ptr_to_skb(ptr);
stats->xdp_bytes += skb->len;
+ if (peer_txq && bql_charged)
+ netdev_tx_completed_queue(peer_txq, 1, VETH_BQL_UNIT);
+
skb = veth_xdp_rcv_skb(rq, skb, bq, stats);
if (skb) {
if (skb_shared(skb) || skb_unclone(skb, GFP_ATOMIC))
@@ -975,7 +1016,7 @@ static int veth_poll(struct napi_struct *napi, int budget)
peer_txq = peer_dev ? netdev_get_tx_queue(peer_dev, queue_idx) : NULL;
xdp_set_return_frame_no_direct();
- done = veth_xdp_rcv(rq, budget, &bq, &stats);
+ done = veth_xdp_rcv(rq, budget, &bq, &stats, peer_txq);
if (stats.xdp_redirect > 0)
xdp_do_flush();
@@ -1073,6 +1114,7 @@ static int __veth_napi_enable(struct net_device *dev)
static void veth_napi_del_range(struct net_device *dev, int start, int end)
{
struct veth_priv *priv = netdev_priv(dev);
+ struct net_device *peer;
int i;
for (i = start; i < end; i++) {
@@ -1091,6 +1133,17 @@ static void veth_napi_del_range(struct net_device *dev, int start, int end)
ptr_ring_cleanup(&rq->xdp_ring, veth_ptr_free);
}
+ /* Reset BQL on peer's txqs: remaining ring items were freed above
+ * without BQL completion, so DQL state must be reset.
+ */
+ peer = rtnl_dereference(priv->peer);
+ if (peer) {
+ int peer_end = min(end, (int)peer->real_num_tx_queues);
+
+ for (i = start; i < peer_end; i++)
+ netdev_tx_reset_queue(netdev_get_tx_queue(peer, i));
+ }
+
for (i = start; i < end; i++) {
page_pool_destroy(priv->rq[i].page_pool);
priv->rq[i].page_pool = NULL;
@@ -1740,6 +1793,7 @@ static void veth_setup(struct net_device *dev)
dev->priv_flags |= IFF_PHONY_HEADROOM;
dev->priv_flags |= IFF_DISABLE_NETPOLL;
dev->lltx = true;
+ dev->bql = true;
dev->netdev_ops = &veth_netdev_ops;
dev->xdp_metadata_ops = &veth_xdp_metadata_ops;
--
2.43.0
^ permalink raw reply related
* [PATCH net-next v3 1/5] net: add dev->bql flag to allow BQL sysfs for IFF_NO_QUEUE devices
From: hawk @ 2026-04-29 17:20 UTC (permalink / raw)
To: netdev
Cc: hawk, kernel-team, Jonas Köppeler, Andrew Lunn,
David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, Jonathan Corbet, Shuah Khan, Kuniyuki Iwashima,
Stanislav Fomichev, Yury Norov, Frederic Weisbecker, Yajun Deng,
linux-doc, linux-kernel
In-Reply-To: <20260429172036.1028526-1-hawk@kernel.org>
From: Jesper Dangaard Brouer <hawk@kernel.org>
Virtual devices with IFF_NO_QUEUE or lltx are excluded from BQL sysfs
by netdev_uses_bql(), since they traditionally lack real hardware
queues. However, some virtual devices like veth implement a real
ptr_ring FIFO with NAPI processing and benefit from BQL to limit
in-flight bytes and reduce latency.
Add a per-device 'bql' bitfield boolean in the priv_flags_slow section
of struct net_device. When set, it overrides the IFF_NO_QUEUE/lltx
exclusion and exposes BQL sysfs entries (/sys/class/net/<dev>/queues/
tx-<n>/byte_queue_limits/). The flag is still gated on CONFIG_BQL.
This allows drivers that use BQL despite being IFF_NO_QUEUE to opt in
to sysfs visibility for monitoring and debugging.
Signed-off-by: Jesper Dangaard Brouer <hawk@kernel.org>
Tested-by: Jonas Köppeler <j.koeppeler@tu-berlin.de>
---
Documentation/networking/net_cachelines/net_device.rst | 1 +
include/linux/netdevice.h | 2 ++
net/core/net-sysfs.c | 8 +++++++-
3 files changed, 10 insertions(+), 1 deletion(-)
diff --git a/Documentation/networking/net_cachelines/net_device.rst b/Documentation/networking/net_cachelines/net_device.rst
index 1c19bb7705df..b775d3235a2d 100644
--- a/Documentation/networking/net_cachelines/net_device.rst
+++ b/Documentation/networking/net_cachelines/net_device.rst
@@ -170,6 +170,7 @@ unsigned_long:1 see_all_hwtstamp_requests
unsigned_long:1 change_proto_down
unsigned_long:1 netns_immutable
unsigned_long:1 fcoe_mtu
+unsigned_long:1 bql netdev_uses_bql(net-sysfs.c)
struct list_head net_notifier_list
struct macsec_ops* macsec_ops
struct udp_tunnel_nic_info* udp_tunnel_nic_info
diff --git a/include/linux/netdevice.h b/include/linux/netdevice.h
index 47417b2d48a4..7a1a491ecdd5 100644
--- a/include/linux/netdevice.h
+++ b/include/linux/netdevice.h
@@ -2048,6 +2048,7 @@ enum netdev_reg_state {
* @change_proto_down: device supports setting carrier via IFLA_PROTO_DOWN
* @netns_immutable: interface can't change network namespaces
* @fcoe_mtu: device supports maximum FCoE MTU, 2158 bytes
+ * @bql: device uses BQL (DQL sysfs) despite having IFF_NO_QUEUE
*
* @net_notifier_list: List of per-net netdev notifier block
* that follow this device when it is moved
@@ -2462,6 +2463,7 @@ struct net_device {
unsigned long change_proto_down:1;
unsigned long netns_immutable:1;
unsigned long fcoe_mtu:1;
+ unsigned long bql:1;
struct list_head net_notifier_list;
diff --git a/net/core/net-sysfs.c b/net/core/net-sysfs.c
index e430645748a7..4360efc8f241 100644
--- a/net/core/net-sysfs.c
+++ b/net/core/net-sysfs.c
@@ -1945,10 +1945,16 @@ static const struct kobj_type netdev_queue_ktype = {
static bool netdev_uses_bql(const struct net_device *dev)
{
+ if (!IS_ENABLED(CONFIG_BQL))
+ return false;
+
+ if (dev->bql)
+ return true;
+
if (dev->lltx || (dev->priv_flags & IFF_NO_QUEUE))
return false;
- return IS_ENABLED(CONFIG_BQL);
+ return true;
}
static int netdev_queue_add_kobject(struct net_device *dev, int index)
--
2.43.0
^ permalink raw reply related
* [PATCH net-next v3 0/5] veth: add Byte Queue Limits (BQL) support
From: hawk @ 2026-04-29 17:20 UTC (permalink / raw)
To: netdev
Cc: hawk, kernel-team, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Simon Horman, Shuah Khan, linux-kselftest,
Chris Arges, Mike Freemon, Toke Høiland-Jørgensen,
Jonas Köppeler, Breno Leitao, Alexei Starovoitov,
Daniel Borkmann, John Fastabend, Stanislav Fomichev, bpf
From: Jesper Dangaard Brouer <hawk@kernel.org>
This series adds BQL (Byte Queue Limits) to the veth driver, reducing
latency by dynamically limiting in-flight packets in the ptr_ring and
moving buffering into the qdisc where AQM algorithms can act on it.
Problem:
veth's 256-entry ptr_ring acts as a "dark buffer" -- packets queued
there are invisible to the qdisc's AQM. Under load, the ring fills
completely (DRV_XOFF backpressure), adding up to 256 packets of
unmanaged latency before the qdisc even sees congestion.
Solution:
BQL (STACK_XOFF) dynamically limits in-flight packets, stopping the
queue before the ring fills. This keeps the ring shallow and pushes
excess packets into the qdisc, where sojourn-based AQM can measure
and drop them.
Test setup: veth pair, UDP flood, 13000 iptables rules in consumer
namespace (slows NAPI-64 cycle to ~6-7ms), ping measures RTT under load.
BQL off BQL on
fq_codel: RTT ~22ms, 4% loss RTT ~1.3ms, 0% loss
sfq: RTT ~24ms, 0% loss RTT ~1.5ms, 0% loss
BQL reduces ping RTT by ~17x for both qdiscs. Consumer throughput
is unchanged (~10K pps) -- BQL adds no overhead.
CoDel bug discovered during BQL development:
Our original motivation for BQL was fq_codel ping loss observed under
load (4-26% depending on NAPI cycle time). Investigating this led us
to discover a bug in the CoDel implementation: codel_dequeue() does
not reset vars->first_above_time when a flow goes empty, contrary to
the reference algorithm. This causes stale CoDel state to persist
across empty periods in fq_codel's per-flow queues, penalizing sparse
flows like ICMP ping. A fix for this has been applied to the net tree:
https://git.kernel.org/netdev/net/c/815980fe6dbb
BQL remains valuable independently: it reduces RTT by ~17x by moving
buffering from the dark ptr_ring into the qdisc. Additionally, BQL
clears STACK_XOFF per-SKB as each packet completes, rather than
batch-waking after 64 packets (DRV_XOFF). This keeps sojourn times
below fq_codel's target, preventing CoDel from entering dropping
state on non-congested flows in the first place.
Key design decisions:
- Charge-under-lock in veth_xdp_rx(): The BQL charge must precede
the ptr_ring produce, because the NAPI consumer can run on another
CPU and complete the SKB immediately after it becomes visible. To
avoid a pre-charge/undo pattern, the charge is done under the
ptr_ring producer_lock after confirming the ring is not full. BQL
is only charged when produce is guaranteed to succeed, keeping
num_queued monotonically increasing. HARD_TX_LOCK already
serializes dql_queued() (veth requires a qdisc for BQL); the
ptr_ring lock additionally would allow noqueue to work correctly.
- Per-SKB BQL tracking via pointer tag: A VETH_BQL_FLAG bit in the
ptr_ring pointer records whether each SKB was BQL-charged. This is
necessary because the qdisc can be replaced live (noqueue->sfq or
vice versa) while SKBs are in-flight -- the completion side must
know the charge state that was decided at enqueue time.
- IFF_NO_QUEUE + BQL coexistence: A new dev->bql flag enables BQL
sysfs exposure for IFF_NO_QUEUE devices that opt in to DQL
accounting, without changing IFF_NO_QUEUE semantics.
Background and acknowledgments:
Mike Freemon reported the veth dark buffer problem internally at
Cloudflare and showed that recompiling the kernel with a ptr_ring
size of 30 (down from 256) made fq_codel work dramatically better.
This was the primary motivation for a proper BQL solution that
achieves the same effect dynamically without a kernel rebuild.
Chris Arges wrote a reproducer for the dark buffer latency problem:
https://github.com/netoptimizer/veth-backpressure-performance-testing
This is where we first observed ping packets being dropped under
fq_codel, which became our secondary motivation for BQL. In
production we switched to SFQ on veth devices as a workaround.
Jonas Koeppeler provided extensive testing and code review.
Together we discovered that the fq_codel ping loss was actually a
12-year-old CoDel bug (stale first_above_time in empty flows), not
caused by the dark buffer itself. A fix has been applied to the net tree:
https://git.kernel.org/netdev/net/c/815980fe6dbb
Patch overview:
1. net: add dev->bql flag to allow BQL sysfs for IFF_NO_QUEUE devices
2. veth: implement Byte Queue Limits (BQL) for latency reduction
3. veth: add tx_timeout watchdog as BQL safety net
4. net: sched: add timeout count to NETDEV WATCHDOG message
5. selftests: net: add veth BQL stress test
Jesper Dangaard Brouer (5):
net: add dev->bql flag to allow BQL sysfs for IFF_NO_QUEUE devices
veth: implement Byte Queue Limits (BQL) for latency reduction
veth: add tx_timeout watchdog as BQL safety net
net: sched: add timeout count to NETDEV WATCHDOG message
selftests: net: add veth BQL stress test
.../networking/net_cachelines/net_device.rst | 1 +
drivers/net/veth.c | 94 +-
include/linux/netdevice.h | 2 +
net/core/net-sysfs.c | 8 +-
net/sched/sch_generic.c | 8 +-
tools/testing/selftests/net/Makefile | 3 +
tools/testing/selftests/net/config | 1 +
tools/testing/selftests/net/napi_poll_hist.bt | 40 +
tools/testing/selftests/net/veth_bql_test.sh | 821 ++++++++++++++++++
.../selftests/net/veth_bql_test_virtme.sh | 124 +++
10 files changed, 1086 insertions(+), 16 deletions(-)
create mode 100644 tools/testing/selftests/net/napi_poll_hist.bt
create mode 100755 tools/testing/selftests/net/veth_bql_test.sh
create mode 100755 tools/testing/selftests/net/veth_bql_test_virtme.sh
V2: https://lore.kernel.org/all/20260413094442.1376022-1-hawk@kernel.org/
Changes since V2:
- Patch 2 (veth BQL): fix syzbot WARNING in veth_napi_del_range():
clamp BQL reset loop to peer's real_num_tx_queues. The loop was
iterating dev->real_num_rx_queues but indexing peer's txq[], which
goes out of bounds when the peer has fewer TX queues (e.g. veth
enslaved to a bond with XDP attached).
- Patch 5 (selftests): fix CONFIG_NET_SCH_SFQ alphabetical ordering
in net/config (Breno Leitao).
V1: https://lore.kernel.org/all/20260324174719.1224337-1-hawk@kernel.org/
Changes since V1:
- Patch 1 (dev->bql flag): add kdoc entry for @bql in struct net_device.
- Patch 2 (veth BQL): charge fixed VETH_BQL_UNIT (1) per packet instead
of skb->len. veth has no link speed; the ptr_ring is packet-indexed.
Byte-based charging lets small packets sneak many entries into the ring.
Testing: min-size packet flood causes 3.7x ping RTT degradation with
skb->len vs no change with fixed-unit charging.
- Patch 3 (tx_timeout watchdog): fix race with peer NAPI: replace
netdev_tx_reset_queue() with clear_bit(STACK_XOFF) + netif_tx_wake_queue()
to avoid dql_reset() racing with concurrent dql_completed().
- Patch 5 (selftests): fix shellcheck warnings and infos:
- Quote variables passed to kill_process and exit.
- Declare and assign local variables separately (SC2155).
- Use read -r to avoid mangling backslashes (SC2162).
- Add shellcheck disable comments for intentional word splitting
(set -- $line, tc $qdisc $opts) and indirect invocation (trap).
- Make iptables-restore failure a hard FAIL instead of continuing.
- Add veth_bql_test.sh to TEST_PROGS in net/Makefile.
- Add veth_bql_test_virtme.sh to TEST_FILES (needs kernel build tree).
- Add napi_poll_hist.bt to TEST_FILES in net/Makefile.
- Add CONFIG_NET_SCH_SFQ=m to net/config (default qdisc is sfq).
- Reduce default test duration from 300s to 30s for kselftest CI.
- Fix virtme wrapper: empty args bug, check vmlinux instead of test path.
- Cover letter: update CoDel fix reference to merged commit in net tree.
Cc: "David S. Miller" <davem@davemloft.net>
Cc: Eric Dumazet <edumazet@google.com>
Cc: Jakub Kicinski <kuba@kernel.org>
Cc: Paolo Abeni <pabeni@redhat.com>
Cc: Simon Horman <horms@kernel.org>
Cc: Shuah Khan <shuah@kernel.org>
Cc: linux-kselftest@vger.kernel.org
Cc: Chris Arges <carges@cloudflare.com>
Cc: Mike Freemon <mfreemon@cloudflare.com>
Cc: Toke Høiland-Jørgensen <toke@toke.dk>
Cc: Jonas Köppeler <j.koeppeler@tu-berlin.de>
Cc: Breno Leitao <leitao@debian.org>
Cc: kernel-team@cloudflare.com
--
2.43.0
^ permalink raw reply
* Re: [PATCH net-next] selftests/net: packetdrill: add tcp_syncookies_ip6_9k
From: Eric Dumazet @ 2026-04-29 17:15 UTC (permalink / raw)
To: Jakub Kicinski
Cc: David S . Miller, Paolo Abeni, Simon Horman, Neal Cardwell,
Kuniyuki Iwashima, netdev, eric.dumazet
In-Reply-To: <CANn89i+KuSf8PhAsf7a=h+tR1c8aLNdiM6NzaBU8tjpoLAfcQA@mail.gmail.com>
On Wed, Apr 29, 2026 at 10:09 AM Eric Dumazet <edumazet@google.com> wrote:
>
> On Wed, Apr 29, 2026 at 9:48 AM Jakub Kicinski <kuba@kernel.org> wrote:
> >
> > On Wed, 29 Apr 2026 08:00:36 +0000 Eric Dumazet wrote:
> > > This test checks syncookie mode is able to reconstruct some
> > > client options when TCP TS are used:
> > >
> > > - wscale option.
> > > - sackOK.
> > > - MSS (in a limited way).
> > > - ECN (not tested, because of limited value).
> >
> > We're getting:
> >
> > # TAP version 13
> > # 1..1
> > # File "/tmp/code_oyWax9", line 223
> > # assert tcpi_snd_wscale = 10, tcpi_snd_wscale
> > # ^
> > # SyntaxError: invalid syntax
> > # tcp_syncookies_ip6_9k.pkt: error executing code: 'python3' returned non-zero status 1
> >
> > do we need to update packetdrill ?
>
> No, this should have been
>
> assert tcpi_snd_wscale == 10, tcpi_snd_wscale
>
> I initially had a value of 8, and decided going to 10 just to make it
> different than the output wscale value.
>
> Sorry for the confusion.
Neal, can you take care of adding TCPI_OPT_SACK to packetdrill ?
Thanks!
diff --git a/gtests/net/packetdrill/code.c b/gtests/net/packetdrill/code.c
index d90bffabcfe6..6d8d20e64d8c 100644
--- a/gtests/net/packetdrill/code.c
+++ b/gtests/net/packetdrill/code.c
@@ -129,6 +129,7 @@ static void write_symbols(struct code_state *code)
emit_var(code, "TCPI_OPT_WSCALE", TCPI_OPT_WSCALE);
emit_var(code, "TCPI_OPT_ECN", TCPI_OPT_ECN);
emit_var(code, "TCPI_OPT_SYN_DATA", TCPI_OPT_SYN_DATA);
+ emit_var(code, "TCPI_OPT_SACK", TCPI_OPT_SACK);
#endif /* linux */
}
^ permalink raw reply related
* Re: [PATCH net-next] selftests/net: packetdrill: add tcp_syncookies_ip6_9k
From: Eric Dumazet @ 2026-04-29 17:09 UTC (permalink / raw)
To: Jakub Kicinski
Cc: David S . Miller, Paolo Abeni, Simon Horman, Neal Cardwell,
Kuniyuki Iwashima, netdev, eric.dumazet
In-Reply-To: <20260429094805.1aa6bf3c@kernel.org>
On Wed, Apr 29, 2026 at 9:48 AM Jakub Kicinski <kuba@kernel.org> wrote:
>
> On Wed, 29 Apr 2026 08:00:36 +0000 Eric Dumazet wrote:
> > This test checks syncookie mode is able to reconstruct some
> > client options when TCP TS are used:
> >
> > - wscale option.
> > - sackOK.
> > - MSS (in a limited way).
> > - ECN (not tested, because of limited value).
>
> We're getting:
>
> # TAP version 13
> # 1..1
> # File "/tmp/code_oyWax9", line 223
> # assert tcpi_snd_wscale = 10, tcpi_snd_wscale
> # ^
> # SyntaxError: invalid syntax
> # tcp_syncookies_ip6_9k.pkt: error executing code: 'python3' returned non-zero status 1
>
> do we need to update packetdrill ?
No, this should have been
assert tcpi_snd_wscale == 10, tcpi_snd_wscale
I initially had a value of 8, and decided going to 10 just to make it
different than the output wscale value.
Sorry for the confusion.
^ permalink raw reply
* [PATCH net-next v2 2/4] r8152: Add support for the RTL8159 chip
From: Birger Koblitz @ 2026-04-29 17:01 UTC (permalink / raw)
To: Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni
Cc: linux-usb, netdev, linux-kernel, Chih Kai Hsu, Birger Koblitz
In-Reply-To: <20260429-rtl8159_net_next-v2-0-bab3cd4e4c66@birger-koblitz.de>
The RTL8157 re-uses the packet descriptor format introduced with the
RTL8157 and other hardware features of the RTL8157 (RTL_VER_16) such
as the SRAM access. The support therefore consists in expanding the
existing RTL8157 code for initialization and USB power management
to also be used for the RTL8159 (RTL_VER_17).
Most of the addiitonal code is added in r8157_hw_phy_cfg() to configure
the RTL8159 PHY.
Add support for the USB device ID of Realtek RTL8157-based adapters. Detect
the RTL8159 as RTL_VER_17 and set it up.
Signed-off-by: Birger Koblitz <mail@birger-koblitz.de>
---
drivers/net/usb/r8152.c | 257 ++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 249 insertions(+), 8 deletions(-)
diff --git a/drivers/net/usb/r8152.c b/drivers/net/usb/r8152.c
index 01e65d845f8732f23427305423e4e270dae775dc..2a07dde289e2240b221664ae5c5bceec35b5a1ef 100644
--- a/drivers/net/usb/r8152.c
+++ b/drivers/net/usb/r8152.c
@@ -1247,6 +1247,7 @@ enum rtl_version {
RTL_VER_14,
RTL_VER_15,
RTL_VER_16,
+ RTL_VER_17,
RTL_VER_MAX
};
@@ -3432,6 +3433,7 @@ static void rtl8152_nic_reset(struct r8152 *tp)
break;
case RTL_VER_16:
+ case RTL_VER_17:
ocp_byte_clr_bits(tp, MCU_TYPE_PLA, PLA_CR, CR_RE | CR_TE);
break;
@@ -3471,6 +3473,9 @@ static void rtl_eee_plus_en(struct r8152 *tp, bool enable)
static void rtl_set_eee_plus(struct r8152 *tp)
{
+ if (tp->version == RTL_VER_17)
+ return rtl_eee_plus_en(tp, false);
+
if (rtl8152_get_speed(tp) & _10bps)
rtl_eee_plus_en(tp, true);
else
@@ -3656,6 +3661,7 @@ static void r8153_set_rx_early_timeout(struct r8152 *tp)
case RTL_VER_13:
case RTL_VER_15:
case RTL_VER_16:
+ case RTL_VER_17:
ocp_write_word(tp, MCU_TYPE_USB, USB_RX_EARLY_TIMEOUT,
640 / 8);
ocp_write_word(tp, MCU_TYPE_USB, USB_RX_EXTRA_AGGR_TMR,
@@ -3700,6 +3706,7 @@ static void r8153_set_rx_early_size(struct r8152 *tp)
ocp_data / 8);
break;
case RTL_VER_16:
+ case RTL_VER_17:
ocp_write_word(tp, MCU_TYPE_USB, USB_RX_EARLY_SIZE,
ocp_data / 16);
break;
@@ -4548,6 +4555,7 @@ static void rtl_clear_bp(struct r8152 *tp, u16 type)
break;
case RTL_VER_14:
case RTL_VER_16:
+ case RTL_VER_17:
default:
ocp_write_word(tp, type, USB_BP2_EN, 0);
bp_num = 16;
@@ -5823,6 +5831,7 @@ static void rtl_eee_enable(struct r8152 *tp, bool enable)
case RTL_VER_13:
case RTL_VER_15:
case RTL_VER_16:
+ case RTL_VER_17:
if (enable) {
r8156_eee_en(tp, true);
ocp_reg_write(tp, OCP_EEE_ADV, tp->eee_adv);
@@ -6894,7 +6903,7 @@ static void rtl8156_down(struct r8152 *tp)
PLA_MCU_SPDWN_EN);
r8153b_u1u2en(tp, false);
- if (tp->version != RTL_VER_16) {
+ if (tp->version < RTL_VER_16) {
r8153_u2p3en(tp, false);
r8153b_power_cut_en(tp, false);
}
@@ -8016,7 +8025,7 @@ static void r8157_hw_phy_cfg(struct r8152 *tp)
/* Advanced Power Saving parameter */
ocp_reg_set_bits(tp, 0xa430, BIT(0) | BIT(1));
- /* aldpsce force mode */
+ /* Disable ALDPS force mode */
ocp_reg_clr_bits(tp, 0xa44a, BIT(2));
switch (tp->version) {
@@ -8140,6 +8149,190 @@ static void r8157_hw_phy_cfg(struct r8152 *tp)
sram2_write_w0w1(tp, 0x807c, 0xff00, 0x5000);
sram2_write_w0w1(tp, 0x809d, 0xff00, 0x5000);
break;
+
+ case RTL_VER_17:
+ /* Disable bypass turn off clk in ALDPS */
+ ocp_byte_clr_bits(tp, MCU_TYPE_PLA, 0xd3c8, BIT(0));
+
+ /* Power level tuning
+ * test mode power level
+ */
+ sram_write_w0w1(tp, 0x8415, 0xff00, 0x9300);
+ /* normal link power level 10G, 5G, 2.5G */
+ sram_write_w0w1(tp, 0x81a3, 0xff00, 0x0f00);
+ sram_write_w0w1(tp, 0x81ae, 0xff00, 0x0f00);
+ sram_write_w0w1(tp, 0x81b9, 0xff00, 0xb900);
+ /* nomal link TX filter */
+ sram2_write_w0w1(tp, 0x83b0, 0x0e00, 0);
+ sram2_write_w0w1(tp, 0x83c5, 0x0e00, 0);
+ sram2_write_w0w1(tp, 0x83da, 0x0e00, 0);
+ sram2_write_w0w1(tp, 0x83ef, 0x0e00, 0);
+
+ /* AFE power saving for 2.5G & 5G */
+ sram_write(tp, 0x8173, 0x8620);
+ sram_write(tp, 0x8175, 0x8671);
+
+ sram_write_w0w1(tp, 0x817c, 0, BIT(13));
+ sram_write_w0w1(tp, 0x8187, 0, BIT(13));
+ sram_write_w0w1(tp, 0x8192, 0, BIT(13));
+ sram_write_w0w1(tp, 0x819d, 0, BIT(13));
+ sram_write_w0w1(tp, 0x81a8, BIT(13), 0);
+ sram_write_w0w1(tp, 0x81b3, BIT(13), 0);
+ sram_write_w0w1(tp, 0x81be, 0, BIT(13));
+
+ sram_write_w0w1(tp, 0x817d, 0xff00, 0xa600);
+ sram_write_w0w1(tp, 0x8188, 0xff00, 0xa600);
+ sram_write_w0w1(tp, 0x8193, 0xff00, 0xa600);
+ sram_write_w0w1(tp, 0x819e, 0xff00, 0xa600);
+ sram_write_w0w1(tp, 0x81a9, 0xff00, 0x1400);
+ sram_write_w0w1(tp, 0x81b4, 0xff00, 0x1400);
+ sram_write_w0w1(tp, 0x81bf, 0xff00, 0xa600);
+
+ /* RFI parameter
+ * disable preset FBE
+ */
+ ocp_reg_clr_bits(tp, 0xaeaa, BIT(5) | BIT(3));
+ /* modify PGA for 5G&10G */
+ sram2_write(tp, 0x84f0, 0x201c);
+ sram2_write(tp, 0x84f2, 0x3117);
+ /* RFI parameter */
+ ocp_reg_write(tp, 0xaec6, 0x0000);
+ ocp_reg_write(tp, 0xae20, 0xffff);
+ ocp_reg_write(tp, 0xaece, 0xffff);
+ ocp_reg_write(tp, 0xaed2, 0xffff);
+ ocp_reg_write(tp, 0xaec8, 0x0000);
+ ocp_reg_clr_bits(tp, 0xaed0, BIT(0));
+ ocp_reg_write(tp, 0xadb8, 0x0150);
+ sram2_write_w0w1(tp, 0x8197, 0xff00, 0x5000);
+ sram2_write_w0w1(tp, 0x8231, 0xff00, 0x5000);
+ sram2_write_w0w1(tp, 0x82cb, 0xff00, 0x5000);
+ sram2_write_w0w1(tp, 0x82cd, 0xff00, 0x5700);
+ sram2_write_w0w1(tp, 0x8233, 0xff00, 0x5700);
+ sram2_write_w0w1(tp, 0x8199, 0xff00, 0x5700);
+
+ sram2_write(tp, 0x815a, 0x0150);
+ sram2_write(tp, 0x81f4, 0x0150);
+ sram2_write(tp, 0x828e, 0x0150);
+ sram2_write(tp, 0x81b1, 0x0000);
+ sram2_write(tp, 0x824b, 0x0000);
+ sram2_write(tp, 0x82e5, 0x0000);
+
+ sram2_write_w0w1(tp, 0x84f7, 0xff00, 0x2800);
+ ocp_reg_set_bits(tp, 0xaec2, BIT(12));
+ sram2_write_w0w1(tp, 0x81b3, 0xff00, 0xad00);
+ sram2_write_w0w1(tp, 0x824d, 0xff00, 0xad00);
+ sram2_write_w0w1(tp, 0x82e7, 0xff00, 0xad00);
+ ocp_reg_w0w1(tp, 0xae4e, 0x000f, 0x0001);
+ sram2_write_w0w1(tp, 0x82ce, 0xf000, 0x4000);
+
+ /* 5G shift sel, default = '04'
+ * 10G shift sel, default = '03'
+ */
+ sram2_write_w0w1(tp, 0x83a5, 0xff00, 0x0400);
+ sram2_write_w0w1(tp, 0x83a6, 0xff00, 0x0400);
+ sram2_write_w0w1(tp, 0x83a7, 0xff00, 0x0400);
+ sram2_write_w0w1(tp, 0x83a8, 0xff00, 0x0400);
+
+ /* XG INRX parameters
+ * RC coefficients
+ */
+ sram2_write(tp, 0x84ac, 0x0000);
+ sram2_write(tp, 0x84ae, 0x0000);
+ sram2_write(tp, 0x84b0, 0xf818);
+ sram2_write_w0w1(tp, 0x84b2, 0xff00, 0x6000);
+ /* Training AAGC PAR (with uc2 patch) */
+ sram2_write(tp, 0x8ffc, 0x6008);
+ sram2_write(tp, 0x8ffe, 0xf450);
+ /* DAC BGK */
+ sram2_write_w0w1(tp, 0x8015, 0, BIT(9));
+ sram2_write_w0w1(tp, 0x8016, 0, BIT(11));
+ sram2_write_w0w1(tp, 0x8fe6, 0xff00, 0x0800);
+ sram2_write(tp, 0x8fe4, 0x2114);
+ /* 10G PBO table */
+ sram2_write(tp, 0x8647, 0xa7b1);
+ sram2_write(tp, 0x8649, 0xbbca);
+ sram2_write_w0w1(tp, 0x864b, 0xff00, 0xdc00);
+ /* 2.5G ado power window size */
+ sram2_write_w0w1(tp, 0x8154, 0xc000, 0x4000);
+ sram2_write_w0w1(tp, 0x8158, 0xc000, 0);
+ /* 10G lock far */
+ sram2_write(tp, 0x826c, 0xffff);
+ sram2_write(tp, 0x826e, 0xffff);
+ /* XG INRX parameter */
+ sram2_write_w0w1(tp, 0x8872, 0xff00, 0x0e00);
+ sram_write_w0w1(tp, 0x8012, 0, BIT(11));
+ sram_write_w0w1(tp, 0x8012, 0, BIT(14));
+ ocp_reg_set_bits(tp, 0xb576, BIT(0));
+ sram_write_w0w1(tp, 0x834a, 0xff00, 0x0700);
+ sram2_write_w0w1(tp, 0x8217, 0x3f00, 0x2a00);
+ sram_write_w0w1(tp, 0x81b1, 0xff00, 0x0b00);
+ sram2_write_w0w1(tp, 0x8fed, 0xff00, 0x4e00);
+ /* Slave about EC mu of datamode AAGC and DAC BG */
+ sram2_write_w0w1(tp, 0x88ac, 0xff00, 0x2300);
+ /* improve UBE */
+ ocp_reg_set_bits(tp, 0xbf0c, 0x7 << 11);
+ /* close Sparse NEC, improve connect 5EUU calble performace */
+ sram2_write_w0w1(tp, 0x88de, 0xff00, 0);
+ /* 5G slave compatibility issue (will include in v10) */
+ sram2_write(tp, 0x80b4, 0x5195);
+
+ /* XG Test Mode
+ * xgtstm_map_tbl for mdi_cap_sel
+ */
+ sram_write(tp, 0x8370, 0x8671);
+ sram_write(tp, 0x8372, 0x86c8);
+ /* xgtstm_amp_map_tbl for REG_IBX_UP_SHIFT_L */
+ sram_write(tp, 0x8401, 0x86c8);
+ sram_write(tp, 0x8403, 0x86da);
+ sram_write_w0w1(tp, 0x8406, 0x1800, 0x1000);
+ sram_write_w0w1(tp, 0x8408, 0x1800, 0x1000);
+ sram_write_w0w1(tp, 0x840a, 0x1800, 0x1000);
+ sram_write_w0w1(tp, 0x840c, 0x1800, 0x1000);
+ sram_write_w0w1(tp, 0x840e, 0x1800, 0x1000);
+ sram_write_w0w1(tp, 0x8410, 0x1800, 0x1000);
+ sram_write_w0w1(tp, 0x8412, 0x1800, 0x1000);
+ sram_write_w0w1(tp, 0x8414, 0x1800, 0x1000);
+ sram_write_w0w1(tp, 0x8416, 0x1800, 0x1000);
+
+ /* Cable Test Patch */
+ sram_write(tp, 0x82bd, 0x1f40);
+
+ /* Thermal sensor parameters */
+ ocp_reg_w0w1(tp, 0xbfb4, 0x07ff, 0x0328);
+ ocp_reg_write(tp, 0xbfb6, 0x3e14);
+
+ /* spdchg_gtx_shape_100M */
+ ocp_reg_write(tp, OCP_SRAM_ADDR, 0x81c4);
+ ocp_reg_write(tp, OCP_SRAM_DATA, 0x003b);
+ ocp_reg_write(tp, OCP_SRAM_DATA, 0x0086);
+ ocp_reg_write(tp, OCP_SRAM_DATA, 0x00b7);
+ ocp_reg_write(tp, OCP_SRAM_DATA, 0x00db);
+ ocp_reg_write(tp, OCP_SRAM_DATA, 0x00fe);
+ ocp_reg_write(tp, OCP_SRAM_DATA, 0x00fe);
+ ocp_reg_write(tp, OCP_SRAM_DATA, 0x00fe);
+ ocp_reg_write(tp, OCP_SRAM_DATA, 0x00fe);
+ ocp_reg_write(tp, OCP_SRAM_DATA, 0x00c3);
+ ocp_reg_write(tp, OCP_SRAM_DATA, 0x0078);
+ ocp_reg_write(tp, OCP_SRAM_DATA, 0x0047);
+ ocp_reg_write(tp, OCP_SRAM_DATA, 0x0023);
+
+ /* lsbmsk_parameters
+ * RL6961_lsbmsk_parameter_250207
+ */
+ sram2_write(tp, 0x88d7, 0x01a0);
+ sram2_write(tp, 0x88d9, 0x01a0);
+ sram2_write(tp, 0x8ffa, 0x002a);
+
+ sram2_write(tp, 0x8fee, 0xffdf);
+ sram2_write(tp, 0x8ff0, 0xffff);
+ sram2_write(tp, 0x8ff2, 0x0a4a);
+ sram2_write(tp, 0x8ff4, 0xaa5a);
+ sram2_write(tp, 0x8ff6, 0x0a4a);
+ sram2_write(tp, 0x8ff8, 0xaa5a);
+
+ sram2_write_w0w1(tp, 0x88d5, 0xff00, 0x0200);
+ break;
+
default:
break;
}
@@ -8175,6 +8368,18 @@ static void r8157_hw_phy_cfg(struct r8152 *tp)
set_bit(PHY_RESET, &tp->flags);
}
+static int r8159_wait_backup_restore(struct r8152 *tp)
+{
+ u32 ocp_data;
+
+ ocp_data = ocp_read_word(tp, MCU_TYPE_USB, USB_MISC_0);
+ if (!(ocp_data & PCUT_STATUS))
+ return 0;
+
+ return poll_timeout_us(ocp_data = ocp_read_word(tp, MCU_TYPE_USB, USB_GPHY_CTRL),
+ ocp_data & BACKUP_RESTRORE, 200, 2000, false);
+}
+
static void r8156_init(struct r8152 *tp)
{
u32 ocp_data;
@@ -8184,14 +8389,14 @@ static void r8156_init(struct r8152 *tp)
if (test_bit(RTL8152_INACCESSIBLE, &tp->flags))
return;
- if (tp->version == RTL_VER_16) {
+ if (tp->version == RTL_VER_16 || tp->version == RTL_VER_17) {
ocp_byte_set_bits(tp, MCU_TYPE_USB, 0xcffe, BIT(3));
ocp_byte_clr_bits(tp, MCU_TYPE_USB, 0xd3ca, BIT(0));
}
ocp_byte_clr_bits(tp, MCU_TYPE_USB, USB_ECM_OP, EN_ALL_SPEED);
- if (tp->version != RTL_VER_16)
+ if (tp->version < RTL_VER_16)
ocp_write_word(tp, MCU_TYPE_USB, USB_SPEED_OPTION, 0);
ocp_word_set_bits(tp, MCU_TYPE_USB, USB_ECM_OPTION, BYPASS_MAC_RESET);
@@ -8205,6 +8410,7 @@ static void r8156_init(struct r8152 *tp)
case RTL_VER_13:
case RTL_VER_15:
case RTL_VER_16:
+ case RTL_VER_17:
r8156b_wait_loading_flash(tp);
break;
default:
@@ -8221,6 +8427,11 @@ static void r8156_init(struct r8152 *tp)
return;
}
+ if (tp->version == RTL_VER_17 && r8159_wait_backup_restore(tp)) {
+ dev_err(&tp->intf->dev, "init failed, backup-restore timed out\n");
+ return;
+ }
+
data = r8153_phy_status(tp, 0);
if (data == PHY_STAT_EXT_INIT) {
ocp_reg_clr_bits(tp, 0xa468, BIT(3) | BIT(1));
@@ -8236,7 +8447,7 @@ static void r8156_init(struct r8152 *tp)
data = r8153_phy_status(tp, PHY_STAT_LAN_ON);
- if (tp->version == RTL_VER_16)
+ if (tp->version >= RTL_VER_16)
r8157_u2p3en(tp, false);
else
r8153_u2p3en(tp, false);
@@ -8247,7 +8458,7 @@ static void r8156_init(struct r8152 *tp)
/* U1/U2/L1 idle timer. 500 us */
ocp_write_word(tp, MCU_TYPE_USB, USB_U1U2_TIMER, 500);
- if (tp->version == RTL_VER_16)
+ if (tp->version >= RTL_VER_16)
r8157_power_cut_en(tp, false);
else
r8153b_power_cut_en(tp, false);
@@ -8294,7 +8505,10 @@ static void r8156_init(struct r8152 *tp)
set_bit(GREEN_ETHERNET, &tp->flags);
/* rx aggregation / 16 bytes Rx descriptor */
- if (tp->version == RTL_VER_16)
+ if (tp->version == RTL_VER_17)
+ ocp_word_clr_bits(tp, MCU_TYPE_USB, USB_USB_CTRL,
+ RX_AGG_DISABLE | RX_DESC_16B | BIT(11));
+ else if (tp->version == RTL_VER_16)
ocp_word_clr_bits(tp, MCU_TYPE_USB, USB_USB_CTRL, RX_AGG_DISABLE | RX_DESC_16B);
else
ocp_word_clr_bits(tp, MCU_TYPE_USB, USB_USB_CTRL, RX_AGG_DISABLE | RX_ZERO_EN);
@@ -8302,7 +8516,7 @@ static void r8156_init(struct r8152 *tp)
if (tp->version < RTL_VER_12)
ocp_byte_set_bits(tp, MCU_TYPE_USB, USB_BMU_CONFIG, ACT_ODMA);
- if (tp->version == RTL_VER_16) {
+ if (tp->version >= RTL_VER_16) {
/* Disable Rx Zero Len */
rtl_bmu_clr_bits(tp, 0x2300, BIT(3));
/* TX descriptor Signature */
@@ -9690,6 +9904,29 @@ static int rtl_ops_init(struct r8152 *tp)
r8157_desc_init(tp);
break;
+ case RTL_VER_17:
+ tp->eee_en = true;
+ tp->eee_adv = MDIO_EEE_100TX | MDIO_EEE_1000T | MDIO_EEE_10GT;
+ tp->eee_adv2 = MDIO_EEE_2_5GT | MDIO_EEE_5GT;
+ ops->init = r8156_init;
+ ops->enable = rtl8156_enable;
+ ops->disable = rtl8153_disable;
+ ops->up = rtl8156_up;
+ ops->down = rtl8156_down;
+ ops->unload = rtl8153_unload;
+ ops->eee_get = r8153_get_eee;
+ ops->eee_set = r8152_set_eee;
+ ops->in_nway = rtl8153_in_nway;
+ ops->hw_phy_cfg = r8157_hw_phy_cfg;
+ ops->autosuspend_en = rtl8157_runtime_enable;
+ ops->change_mtu = rtl8156_change_mtu;
+ tp->rx_buf_sz = 48 * 1024;
+ tp->support_2500full = 1;
+ tp->support_5000full = 1;
+ tp->support_10000full = 1;
+ r8157_desc_init(tp);
+ break;
+
default:
ret = -ENODEV;
dev_err(&tp->intf->dev, "Unknown Device\n");
@@ -9843,6 +10080,9 @@ static u8 __rtl_get_hw_ver(struct usb_device *udev)
case 0x1030:
version = RTL_VER_16;
break;
+ case 0x2020:
+ version = RTL_VER_17;
+ break;
default:
version = RTL_VER_UNKNOWN;
dev_info(&udev->dev, "Unknown version 0x%04x\n", ocp_data);
@@ -10160,6 +10400,7 @@ static const struct usb_device_id rtl8152_table[] = {
{ USB_DEVICE(VENDOR_ID_REALTEK, 0x8155) },
{ USB_DEVICE(VENDOR_ID_REALTEK, 0x8156) },
{ USB_DEVICE(VENDOR_ID_REALTEK, 0x8157) },
+ { USB_DEVICE(VENDOR_ID_REALTEK, 0x815a) },
/* Microsoft */
{ USB_DEVICE(VENDOR_ID_MICROSOFT, 0x07ab) },
--
2.47.3
^ permalink raw reply related
* [PATCH net-next v2 4/4] r8152: Add firmware upload capability for RTL8157/RTL8159
From: Birger Koblitz @ 2026-04-29 17:01 UTC (permalink / raw)
To: Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni
Cc: linux-usb, netdev, linux-kernel, Chih Kai Hsu, Birger Koblitz
In-Reply-To: <20260429-rtl8159_net_next-v2-0-bab3cd4e4c66@birger-koblitz.de>
The RTL8159 (RTL_VER_17) requires firmware for its PHY in order to work
at connection speeds > 5GBit. Add support for uploading firmware for
the PHY using the existing rtl8152_apply_firmware() function
in r8157_hw_phy_cfg() and set up the correct names for the firmware
files.
This also adds support for uploading firmware for the RTL8157
(RTL_VER_16) PHY, for which firmware is however not strictly necessary
to work. Still, this allows to upload newer versions of the firmware used
by this chip, e.g. to improve interoperability.
If no firmware is found, both the RTL8157 and the RTL8159 will continue
to work.
Signed-off-by: Birger Koblitz <mail@birger-koblitz.de>
---
drivers/net/usb/r8152.c | 15 ++++++++++++++-
1 file changed, 14 insertions(+), 1 deletion(-)
diff --git a/drivers/net/usb/r8152.c b/drivers/net/usb/r8152.c
index 9fcad3dac76f8aa76ef074cabd3b08348dc234bc..8d880afd88c9392cd873a5015a0d7feee538ac7f 100644
--- a/drivers/net/usb/r8152.c
+++ b/drivers/net/usb/r8152.c
@@ -4663,10 +4663,11 @@ static bool rtl8152_is_fw_phy_speed_up_ok(struct r8152 *tp, struct fw_phy_speed_
case RTL_VER_11:
case RTL_VER_12:
case RTL_VER_14:
- case RTL_VER_16:
goto out;
case RTL_VER_13:
case RTL_VER_15:
+ case RTL_VER_16:
+ case RTL_VER_17:
default:
break;
}
@@ -7996,12 +7997,14 @@ static void r8157_hw_phy_cfg(struct r8152 *tp)
data = r8153_phy_status(tp, 0);
switch (data) {
case PHY_STAT_EXT_INIT:
+ rtl8152_apply_firmware(tp, true);
ocp_reg_clr_bits(tp, 0xa466, BIT(0));
ocp_reg_clr_bits(tp, 0xa468, BIT(3) | BIT(1));
break;
case PHY_STAT_LAN_ON:
case PHY_STAT_PWRDN:
default:
+ rtl8152_apply_firmware(tp, false);
break;
}
@@ -9949,6 +9952,8 @@ static int rtl_ops_init(struct r8152 *tp)
#define FIRMWARE_8153C_1 "rtl_nic/rtl8153c-1.fw"
#define FIRMWARE_8156A_2 "rtl_nic/rtl8156a-2.fw"
#define FIRMWARE_8156B_2 "rtl_nic/rtl8156b-2.fw"
+#define FIRMWARE_8157_1 "rtl_nic/rtl8157-1.fw"
+#define FIRMWARE_8159_1 "rtl_nic/rtl8159-1.fw"
MODULE_FIRMWARE(FIRMWARE_8153A_2);
MODULE_FIRMWARE(FIRMWARE_8153A_3);
@@ -9957,6 +9962,8 @@ MODULE_FIRMWARE(FIRMWARE_8153B_2);
MODULE_FIRMWARE(FIRMWARE_8153C_1);
MODULE_FIRMWARE(FIRMWARE_8156A_2);
MODULE_FIRMWARE(FIRMWARE_8156B_2);
+MODULE_FIRMWARE(FIRMWARE_8157_1);
+MODULE_FIRMWARE(FIRMWARE_8159_1);
static int rtl_fw_init(struct r8152 *tp)
{
@@ -9995,6 +10002,12 @@ static int rtl_fw_init(struct r8152 *tp)
rtl_fw->pre_fw = r8153b_pre_firmware_1;
rtl_fw->post_fw = r8153c_post_firmware_1;
break;
+ case RTL_VER_16:
+ rtl_fw->fw_name = FIRMWARE_8157_1;
+ break;
+ case RTL_VER_17:
+ rtl_fw->fw_name = FIRMWARE_8159_1;
+ break;
default:
break;
}
--
2.47.3
^ permalink raw reply related
* [PATCH net-next v2 3/4] r8152: Add irq mitigation for RTL8157/9
From: Birger Koblitz @ 2026-04-29 17:01 UTC (permalink / raw)
To: Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni
Cc: linux-usb, netdev, linux-kernel, Chih Kai Hsu, Birger Koblitz
In-Reply-To: <20260429-rtl8159_net_next-v2-0-bab3cd4e4c66@birger-koblitz.de>
Add interrupt mitigation code for both RTL8157 and RTL8159 that prevents
USB interrupt callbacks with urb->status ESHUTDOWN being triggered. While the
issue is rarely seen on the RTL8157, without the mitigation, it is
common on the RTL8159:
[273.561863] r8152 7-1:1.0 enx88c9b3b5xxxx: Stop submitting intr, status -108
Signed-off-by: Birger Koblitz <mail@birger-koblitz.de>
---
drivers/net/usb/r8152.c | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/drivers/net/usb/r8152.c b/drivers/net/usb/r8152.c
index 2a07dde289e2240b221664ae5c5bceec35b5a1ef..9fcad3dac76f8aa76ef074cabd3b08348dc234bc 100644
--- a/drivers/net/usb/r8152.c
+++ b/drivers/net/usb/r8152.c
@@ -8452,6 +8452,12 @@ static void r8156_init(struct r8152 *tp)
else
r8153_u2p3en(tp, false);
+ if (tp->version >= RTL_VER_16) {
+ /* Disable Interrupt Mitigation */
+ ocp_byte_clr_bits(tp, MCU_TYPE_USB, 0xcf04,
+ BIT(0) | BIT(1) | BIT(2) | BIT(7));
+ }
+
/* MSC timer = 0xfff * 8ms = 32760 ms */
ocp_write_word(tp, MCU_TYPE_USB, USB_MSC_TIMER, 0x0fff);
--
2.47.3
^ permalink raw reply related
* [PATCH net-next v2 1/4] r8152: Add support for 10Gbit Link Speeds and EEE
From: Birger Koblitz @ 2026-04-29 17:01 UTC (permalink / raw)
To: Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni
Cc: linux-usb, netdev, linux-kernel, Chih Kai Hsu, Birger Koblitz,
Andrew Lunn
In-Reply-To: <20260429-rtl8159_net_next-v2-0-bab3cd4e4c66@birger-koblitz.de>
The RTL8159 supports 10GBit Link speeds. Add support for this speed
in the setup and setting/getting through ethtool. Also add 10GBit EEE.
Add functionality for setup and ethtool get/set methods.
Signed-off-by: Birger Koblitz <mail@birger-koblitz.de>
Reviewed-by: Andrew Lunn <andrew@lunn.ch>
---
drivers/net/usb/r8152.c | 58 ++++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 55 insertions(+), 3 deletions(-)
diff --git a/drivers/net/usb/r8152.c b/drivers/net/usb/r8152.c
index 7337bf1b7d6ad03572edbc492706c07a8f58760f..01e65d845f8732f23427305423e4e270dae775dc 100644
--- a/drivers/net/usb/r8152.c
+++ b/drivers/net/usb/r8152.c
@@ -621,6 +621,7 @@ enum spd_duplex {
FORCE_1000M_FULL,
NWAY_2500M_FULL,
NWAY_5000M_FULL,
+ NWAY_10000M_FULL,
};
/* OCP_ALDPS_CONFIG */
@@ -742,6 +743,7 @@ enum spd_duplex {
#define BP4_SUPER_ONLY 0x1578 /* RTL_VER_04 only */
enum rtl_register_content {
+ _10000bps = BIT(14),
_5000bps = BIT(12),
_2500bps = BIT(10),
_1250bps = BIT(9),
@@ -757,6 +759,8 @@ enum rtl_register_content {
#define is_speed_2500(_speed) (((_speed) & (_2500bps | LINK_STATUS)) == (_2500bps | LINK_STATUS))
#define is_speed_5000(_speed) (((_speed) & (_5000bps | LINK_STATUS)) == (_5000bps | LINK_STATUS))
+#define is_speed_10000(_speed) (((_speed) & (_10000bps | LINK_STATUS)) \
+ == (_10000bps | LINK_STATUS))
#define is_flow_control(_speed) (((_speed) & (_tx_flow | _rx_flow)) == (_tx_flow | _rx_flow))
#define RTL8152_MAX_TX 4
@@ -1008,6 +1012,7 @@ struct r8152 {
u32 support_2500full:1;
u32 support_5000full:1;
+ u32 support_10000full:1;
u32 lenovo_macpassthru:1;
u32 dell_tb_rx_agg_bug:1;
u16 ocp_base;
@@ -1260,6 +1265,7 @@ enum tx_csum_stat {
#define RTL_ADVERTISED_1000_FULL BIT(5)
#define RTL_ADVERTISED_2500_FULL BIT(6)
#define RTL_ADVERTISED_5000_FULL BIT(7)
+#define RTL_ADVERTISED_10000_FULL BIT(8)
/* Maximum number of multicast addresses to filter (vs. Rx-all-multicast).
* The RTL chips use a 64 element hash table based on the Ethernet CRC.
@@ -5773,6 +5779,11 @@ static void r8156_eee_en(struct r8152 *tp, bool enable)
else
config &= ~MDIO_EEE_5GT;
+ if (enable && (tp->eee_adv2 & MDIO_EEE_10GT))
+ config |= MDIO_EEE_10GT;
+ else
+ config &= ~MDIO_EEE_10GT;
+
ocp_reg_write(tp, OCP_EEE_ADV2, config);
}
@@ -6513,6 +6524,9 @@ static int rtl8152_set_speed(struct r8152 *tp, u8 autoneg, u32 speed, u8 duplex,
if (tp->support_5000full)
support |= RTL_ADVERTISED_5000_FULL;
+
+ if (tp->support_10000full)
+ support |= RTL_ADVERTISED_10000_FULL;
}
advertising &= support;
@@ -6559,9 +6573,10 @@ static int rtl8152_set_speed(struct r8152 *tp, u8 autoneg, u32 speed, u8 duplex,
r8152_mdio_write(tp, MII_CTRL1000, new1);
}
- if (tp->support_2500full || tp->support_5000full) {
+ if (tp->support_2500full || tp->support_5000full || tp->support_10000full) {
orig = ocp_reg_read(tp, OCP_10GBT_CTRL);
- new1 = orig & ~(MDIO_AN_10GBT_CTRL_ADV2_5G | MDIO_AN_10GBT_CTRL_ADV5G);
+ new1 = orig & ~(MDIO_AN_10GBT_CTRL_ADV2_5G | MDIO_AN_10GBT_CTRL_ADV5G
+ | MDIO_AN_10GBT_CTRL_ADV10G);
if (advertising & RTL_ADVERTISED_2500_FULL) {
new1 |= MDIO_AN_10GBT_CTRL_ADV2_5G;
@@ -6573,6 +6588,11 @@ static int rtl8152_set_speed(struct r8152 *tp, u8 autoneg, u32 speed, u8 duplex,
tp->ups_info.speed_duplex = NWAY_5000M_FULL;
}
+ if (advertising & RTL_ADVERTISED_10000_FULL) {
+ new1 |= MDIO_AN_10GBT_CTRL_ADV10G;
+ tp->ups_info.speed_duplex = NWAY_10000M_FULL;
+ }
+
if (orig != new1)
ocp_reg_write(tp, OCP_10GBT_CTRL, new1);
}
@@ -8723,7 +8743,10 @@ int rtl8152_get_link_ksettings(struct net_device *netdev,
linkmode_mod_bit(ETHTOOL_LINK_MODE_5000baseT_Full_BIT,
cmd->link_modes.supported, tp->support_5000full);
- if (tp->support_2500full || tp->support_5000full) {
+ linkmode_mod_bit(ETHTOOL_LINK_MODE_10000baseT_Full_BIT,
+ cmd->link_modes.supported, tp->support_10000full);
+
+ if (tp->support_2500full || tp->support_5000full || tp->support_10000full) {
u16 ocp_10gbt_ctrl = ocp_reg_read(tp, OCP_10GBT_CTRL);
u16 ocp_10gbt_stat = ocp_reg_read(tp, OCP_10GBT_STAT);
@@ -8752,6 +8775,19 @@ int rtl8152_get_link_ksettings(struct net_device *netdev,
if (is_speed_5000(rtl8152_get_speed(tp)))
cmd->base.speed = SPEED_5000;
}
+
+ if (tp->support_10000full) {
+ linkmode_mod_bit(ETHTOOL_LINK_MODE_10000baseT_Full_BIT,
+ cmd->link_modes.advertising,
+ ocp_10gbt_ctrl & MDIO_AN_10GBT_CTRL_ADV10G);
+
+ linkmode_mod_bit(ETHTOOL_LINK_MODE_10000baseT_Full_BIT,
+ cmd->link_modes.lp_advertising,
+ ocp_10gbt_stat & MDIO_AN_10GBT_STAT_LP10G);
+
+ if (is_speed_10000(rtl8152_get_speed(tp)))
+ cmd->base.speed = SPEED_10000;
+ }
}
mutex_unlock(&tp->control);
@@ -8805,6 +8841,10 @@ static int rtl8152_set_link_ksettings(struct net_device *dev,
cmd->link_modes.advertising))
advertising |= RTL_ADVERTISED_5000_FULL;
+ if (test_bit(ETHTOOL_LINK_MODE_10000baseT_Full_BIT,
+ cmd->link_modes.advertising))
+ advertising |= RTL_ADVERTISED_10000_FULL;
+
mutex_lock(&tp->control);
ret = rtl8152_set_speed(tp, cmd->base.autoneg, cmd->base.speed,
@@ -8968,6 +9008,13 @@ static int r8153_get_eee(struct r8152 *tp, struct ethtool_keee *eee)
linkmode_set_bit(ETHTOOL_LINK_MODE_5000baseT_Full_BIT, common);
}
+ if (tp->support_10000full) {
+ linkmode_set_bit(ETHTOOL_LINK_MODE_10000baseT_Full_BIT, eee->supported);
+
+ if (speed & _10000bps)
+ linkmode_set_bit(ETHTOOL_LINK_MODE_10000baseT_Full_BIT, common);
+ }
+
eee->eee_enabled = tp->eee_en;
if (speed & _1000bps)
@@ -9982,6 +10029,11 @@ static int rtl8152_probe_once(struct usb_interface *intf,
tp->speed = SPEED_5000;
tp->advertising |= RTL_ADVERTISED_5000_FULL;
}
+ if (tp->support_10000full &&
+ tp->udev->speed >= USB_SPEED_SUPER) {
+ tp->speed = SPEED_10000;
+ tp->advertising |= RTL_ADVERTISED_10000_FULL;
+ }
tp->advertising |= RTL_ADVERTISED_1000_FULL;
}
tp->duplex = DUPLEX_FULL;
--
2.47.3
^ permalink raw reply related
* [PATCH net-next v2 0/4] r8152: Add support for the RTL8159 10Gbit USB Ethernet chip
From: Birger Koblitz @ 2026-04-29 17:01 UTC (permalink / raw)
To: Andrew Lunn, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni
Cc: linux-usb, netdev, linux-kernel, Chih Kai Hsu, Birger Koblitz,
Andrew Lunn
Add support for the RTL8159, which is a 10GBit USB-Ethernet adapter
chip in the RTL815x family of chips.
The RTL8159 re-uses the frame descriptor format and SRAM2 access introduced
with the RTL8157 as well as most of the setup and PM logic of the RTL8157.
The module was tested with a Lekuo DR59R11 USB-C 10GbE Ethernet Adapter:
[ 2502.906947] usb 2-1: new SuperSpeed USB device number 3 using xhci_hcd
[ 2502.927859] usb 2-1: New USB device found, idVendor=0bda, idProduct=815a, bcdDevice=30.00
[ 2502.927867] usb 2-1: New USB device strings: Mfr=1, Product=2, SerialNumber=7
[ 2502.927871] usb 2-1: Product: USB 10/100/1G/2.5G/5G/10G LAN
[ 2502.927873] usb 2-1: Manufacturer: Realtek
[ 2502.927875] usb 2-1: SerialNumber: 000388C9B3B5XXXX
[ 2503.063745] r8152-cfgselector 2-1: reset SuperSpeed USB device number 3 using xhci_hcd
[ 2503.123876] r8152 2-1:1.0: Requesting firmware: rtl_nic/rtl8159-1.fw
[ 2503.126267] r8152 2-1:1.0: PHY firmware installed 0 to be loaded: 20
[ 2503.156265] r8152 2-1:1.0: load rtl8159-1 v1 2026/01/01 successfully
[ 2503.270729] r8152 2-1:1.0 eth0: v1.12.13
[ 2503.289349] r8152 2-1:1.0 enx88c9b3b5xxxx: renamed from eth0
[ 2507.777055] r8152 2-1:1.0 enx88c9b3b5xxxx: carrier on
The RTL8159 adapter was tested against an AQC107 PCIe-card supporting
10GBit/s and an RTL8157 5Gbit USB-Ethernet adapter supporting 5GBit/s for
performance, link speed and EEE negotiation. Using USB3.2 Gen 2 (20GBit) with
the RTL8159 USB adapter and running iperf3 against the AQC107 PCIe
card resulted in 8.96 Gbits/sec transfer speed.
The code is based on the out-of-tree r8152 driver published by Realtek under
the GPL.
The RTL8159 requires firmware for the PHY in order to achieve a 10GBit link
speed. Without firmware, only 5GBit were achieved. The firmware can be
extracted from the out-of-tree r8152 driver-code where it is stored in the
ram17 u8-array. Code is added to use the existing firmware upload mechanism
of the driver for the RTL8157/9 PHY firmware code. The firmware will be
submitted separately to linux-firmware.
Signed-off-by: Birger Koblitz <mail@birger-koblitz.de>
---
Changes in v2:
- Correct formatting of comments
- Order case statement values correctly
- Add error message when backup-restore fails
- Correct commit message of support for firmware upload
- Link to v1: https://lore.kernel.org/r/20260428-rtl8159_net_next-v1-0-52d03927b46f@birger-koblitz.de
---
Birger Koblitz (4):
r8152: Add support for 10Gbit Link Speeds and EEE
r8152: Add support for the RTL8159 chip
r8152: Add irq mitigation for RTL8157/9
r8152: Add firmware upload capability for RTL8157/RTL8159
drivers/net/usb/r8152.c | 336 ++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 324 insertions(+), 12 deletions(-)
---
base-commit: 35c2c39832e569449b9192fa1afbbc4c66227af7
change-id: 20260427-rtl8159_net_next-4f778a614fa7
Best regards,
--
Birger Koblitz <mail@birger-koblitz.de>
^ permalink raw reply
* Re: [PATCH 1/2] net: thunderbolt: Call tbnet_stop() upon shutdown
From: Mika Westerberg @ 2026-04-29 16:57 UTC (permalink / raw)
To: Andrew Lunn
Cc: Jakub Kicinski, netdev, YehezkelShB, andrew+netdev, davem,
edumazet, pabeni
In-Reply-To: <8a52ab2d-9cfa-4478-bbda-c910c2b0f726@lunn.ch>
On Wed, Apr 29, 2026 at 03:31:43PM +0200, Andrew Lunn wrote:
> On Wed, Apr 29, 2026 at 07:30:46AM +0200, Mika Westerberg wrote:
> > On Tue, Apr 28, 2026 at 06:12:24PM -0700, Jakub Kicinski wrote:
> > > This is an AI-generated review of your patch. The human sending this
> > > email has considered the AI review valid, or at least plausible.
> > > ---
> > > net: thunderbolt: Call tbnet_stop() upon shutdown
> > >
> > > This commit updates the shutdown callback for the Thunderbolt network service
> > > driver to call tbnet_stop() if the interface is running. This ensures the
> > > service drivers release the rings upon shutdown.
> > >
> > > > diff --git a/drivers/net/thunderbolt/main.c b/drivers/net/thunderbolt/main.c
> > > > index 7aae5d915a1e..631af237998d 100644
> > > > --- a/drivers/net/thunderbolt/main.c
> > > > +++ b/drivers/net/thunderbolt/main.c
> > > > @@ -1417,7 +1417,10 @@ static void tbnet_remove(struct tb_service *svc)
> > > >
> > > > static void tbnet_shutdown(struct tb_service *svc)
> > > > {
> > > > - tbnet_tear_down(tb_service_get_drvdata(svc), true);
> > > > + struct tbnet *net = tb_service_get_drvdata(svc);
> > > > +
> > > > + if (netif_running(net->dev))
> > > > + tbnet_stop(net->dev);
> > >
> > > Does calling tbnet_stop() directly bypass the network stack teardown sequence?
> >
> > This is called when the machine is rebooted or shut down to quiesce the
> > device. I don't think the network stack has anything similar?
>
> You mean that network interfaces are left running while the machine
> reboots, potentially DMAing receive packets over the bootloader or
> freshly loaded kernel? Does that seem reasonable?
Hey! 20+ years ago I spent quite amount of time to investigate an issue
exactly like that where ramdisk randomly corrupted. Not fun.
> Don't you think the core will somehow call the .close function? I cannot
> point to the code doing that without a bit if searching, but i think it
> probably exists.
If I do 'reboot -f' .ndo_stop (I think that's what you mean by .close) is
not called.
If I do just 'reboot' it is called. I suspect based on my added WARN_ON()
that it is done by userspace shutdown scripts.
^ permalink raw reply
* Re: [Intel-wired-lan] [PATCH] i40e: Fix i40e_debug() to use struct i40e_hw argument
From: Mohamed Khalfella @ 2026-04-29 16:52 UTC (permalink / raw)
To: Paul Menzel
Cc: Tony Nguyen, Przemek Kitszel, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, intel-wired-lan, netdev,
linux-kernel
In-Reply-To: <558971b7-caea-471a-8fe2-73ba6cc0790a@molgen.mpg.de>
On Wed 2026-04-29 13:02:00 +0200, Paul Menzel wrote:
> Dear Mohamed,
>
>
> Thank you for your patch.
>
> Am 28.04.26 um 20:14 schrieb Mohamed Khalfella:
> > i40e_debug() macro takes struct i40e_hw *h as first argument. But the
> > macro body uses hw instead of h. This has been working so far because hw
> > happen to be the name of the variable in the context where the marco is
>
> marco → ma*cr*o
Good catch. Also 'happen' should be 'happens'
>
> > expanded. Fix the macro to use the passed argument.
>
> I’d add a Fixes: tag, but the maintainers might have more input.
Yes, I should have added Fixes: tag. I will leave it to the maintainer
to decide if v2 is needed to fix the spelling mistakes and add Fixes
tag.
Fixes: 5dfd37c37a44 ("i40e: Split i40e_osdep.h")
>
> > Signed-off-by: Mohamed Khalfella <mkhalfella@purestorage.com>
> > ---
> > drivers/net/ethernet/intel/i40e/i40e_debug.h | 2 +-
> > 1 file changed, 1 insertion(+), 1 deletion(-)
> >
> > diff --git a/drivers/net/ethernet/intel/i40e/i40e_debug.h b/drivers/net/ethernet/intel/i40e/i40e_debug.h
> > index e9871dfb32bd..01fd70db9086 100644
> > --- a/drivers/net/ethernet/intel/i40e/i40e_debug.h
> > +++ b/drivers/net/ethernet/intel/i40e/i40e_debug.h
> > @@ -42,7 +42,7 @@ struct device *i40e_hw_to_dev(struct i40e_hw *hw);
> > #define i40e_debug(h, m, s, ...) \
> > do { \
> > if (((m) & (h)->debug_mask)) \
> > - dev_info(i40e_hw_to_dev(hw), s, ##__VA_ARGS__); \
> > + dev_info(i40e_hw_to_dev(h), s, ##__VA_ARGS__); \
> > } while (0)
> >
> > #endif /* _I40E_DEBUG_H_ */
>
> Reviewed-by: Paul Menzel <pmenzel@molgen.mpg.de>
>
>
> Kind regards,
>
> Paul
>
>
> PS: gemini/gemini-3.1-pro-preview embargoed it’s review until the
> evening [1].
>
>
> [1]:
> https://sashiko.dev/#/patchset/20260428181450.2622899-1-mkhalfella%40purestorage.com
^ permalink raw reply
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox