* [PATCH net 0/2] veth: fix txqs that are never woken after backpressure
@ 2026-11-13 10:46 Simon Schippers
2026-11-13 10:46 ` [PATCH net 1/2] veth: wake a stopped peer txq beyond real_num_tx_queues Simon Schippers
2026-11-13 10:46 ` [PATCH net 2/2] veth: wake the peer txq when deleting NAPI Simon Schippers
0 siblings, 2 replies; 4+ messages in thread
From: Simon Schippers @ 2026-11-13 10:46 UTC (permalink / raw)
To: netdev
Cc: Andrew Lunn, David S . Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Jesper Dangaard Brouer, Toshiaki Makita,
Jonas Köppeler, Alexei Starovoitov, Daniel Borkmann,
John Fastabend, Stanislav Fomichev, simon.schippers, bpf,
linux-kernel
While debugging the veth BQL series [1], I found two ways in which a veth
txq that veth_xmit() stopped on a full ptr_ring is never woken again.
Patch 1 lets veth_poll() wake txqs beyond real_num_tx_queues, as a txq
stopped before a TX channel shrink is otherwise left stopped.
Patch 2 wakes the peer txq when NAPI is deleted, as veth_xmit() can stop
it after the last poll.
The reproducers are below the --- of each patch. Patch 1 reproduces
without changes to the kernel, patch 2 needs a debug-only udelay() to
widen the race window.
Claude Opus 5.5 was used heavily for this series, to find and reproduce
both bugs and to write the patches.
[1] https://lore.kernel.org/netdev/20260612083530.1650245-1-hawk@kernel.org/
Simon Schippers (2):
veth: wake a stopped peer txq beyond real_num_tx_queues
veth: wake the peer txq when deleting NAPI
drivers/net/veth.c | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
base-commit: ead6fa9a0ab194f02a2f36d634b193e9c5f9a1c6
--
2.43.0
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH net 1/2] veth: wake a stopped peer txq beyond real_num_tx_queues
2026-11-13 10:46 [PATCH net 0/2] veth: fix txqs that are never woken after backpressure Simon Schippers
@ 2026-11-13 10:46 ` Simon Schippers
2026-11-14 10:47 ` sashiko-bot
2026-11-13 10:46 ` [PATCH net 2/2] veth: wake the peer txq when deleting NAPI Simon Schippers
1 sibling, 1 reply; 4+ messages in thread
From: Simon Schippers @ 2026-11-13 10:46 UTC (permalink / raw)
To: netdev
Cc: Andrew Lunn, David S . Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Jesper Dangaard Brouer, Toshiaki Makita,
Jonas Köppeler, Alexei Starovoitov, Daniel Borkmann,
John Fastabend, Stanislav Fomichev, simon.schippers, bpf,
linux-kernel
Since commit dc82a33297fc ("veth: apply qdisc backpressure on full
ptr_ring to reduce TX drops"), only the peer's veth_poll() wakes a txq
that veth_xmit() stopped on a full ptr_ring. veth_poll() skips txqs at
or above the peer's real_num_tx_queues.
If the peer shrinks its TX channels while such a txq is stopped, nothing
wakes it. netif_set_real_num_tx_queues() keeps its state, so once the
channels are grown back, its first skb is requeued forever. With a single
root qdisc this blocks all transmission of the device.
Look the txq up below num_tx_queues instead. Waking a txq that is not in
use is harmless.
Reproduced in virtme-ng with a UDP flood from veth0 into veth1, which has
GRO, while switching veth0 between one and two TX channels: without this
patch veth0 stopped transmitting within 11 cycles in 3 of 3 runs, with
it veth0 kept transmitting through 30 cycles in 3 of 3 runs.
Fixes: dc82a33297fc ("veth: apply qdisc backpressure on full ptr_ring to reduce TX drops")
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Simon Schippers <simon.schippers@tu-dortmund.de>
---
Notes:
Reproducer (needs >= 4 CPUs, no debug hook needed). Run as root:
./repro-veth-chan-shrink-txq-stall.sh 30
repro-veth-chan-shrink-txq-stall.sh:
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
#
# Reproducer: a veth txq stopped on a full ptr_ring stays stopped after the
# device shrinks its TX channels and grows them back.
#
# veth0 (init netns, 2 txqs, pfifo qdisc, UDP flood) -> veth1 (netns rx, GRO
# on, threaded NAPI). The ring fills, veth_xmit() returns NETDEV_TX_BUSY and
# stops a veth0 txq until veth1's veth_poll() wakes it. veth0 is switched to
# one TX channel and back to two. If txq 1 is stopped at the shrink,
# veth_poll() skips it as it is beyond real_num_tx_queues. Once it is back in
# use, its first skb is requeued forever and blocks the qdisc.
#
# Needs at least 4 CPUs. No debug hook needed.
#
# Usage: repro-veth-chan-shrink-txq-stall.sh [cycles]
#
# RULES (default 1000) nft rules in netns rx slow the consumer down.
#
# Exits 1 if veth0 got stuck, 0 if it survived all cycles, 2 if the ring never
# filled.
ITER=${1:-30}
RULES=${RULES:-1000}
cleanup() {
kill $(jobs -p) 2>/dev/null
wait 2>/dev/null
ip link del veth0 2>/dev/null
ip netns del rx 2>/dev/null
}
trap cleanup EXIT
sent() { tc -s qdisc show dev veth0 | awk '/Sent/ { print $4; exit }'; }
requeues() { tc -s qdisc show dev veth0 | sed -n 's/.*requeues \([0-9]*\).*/\1/p' | head -1; }
rx() { ip netns exec rx "$@"; }
ip netns add rx
ip link add veth0 numtxqueues 2 numrxqueues 2 type veth \
peer name veth1 numtxqueues 2 numrxqueues 2 netns rx
ip addr add 10.0.0.1/24 dev veth0
rx ip addr add 10.0.0.2/24 dev veth1
ip link set veth0 up
rx ip link set veth1 up
tc qdisc replace dev veth0 root pfifo limit 1000
# veth only takes skbs to the ring without TSO, unless the peer has UDP GRO
ethtool -K veth0 tso off
rx ethtool -K veth1 gro on
rx sh -c 'echo 1 > /sys/class/net/veth1/threaded'
for p in $(pgrep napi/veth1); do taskset -pc 3 "$p" >/dev/null; done
# Slow down the consumer so that the 256-entry rings fill up
rx nft add table inet slow
rx nft add chain inet slow in '{ type filter hook input priority 0; }'
for i in $(seq 1 "$RULES"); do
echo "add rule inet slow in meta mark $i drop"
done | rx nft -f -
ping -c 1 -W 1 10.0.0.2 >/dev/null
# One socket per flooder, so that they hash onto both txqs. Unlike pktgen they
# keep sending while set_channels turns the carrier off.
for f in 1 2 3 4; do
taskset -c $(((f - 1) % 3)) python3 -c '
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setblocking(False)
while True:
try:
s.sendto(b"x" * 18, ("10.0.0.2", 9))
except OSError:
pass' &
done
sleep 2
echo "kernel $(uname -r), cycles $ITER"
echo "after 2s flood: sent $(sent) pkts, requeues $(requeues)"
if [ "$(requeues)" = 0 ]; then
echo "ring never filled, raise RULES"
exit 2
fi
for i in $(seq 1 "$ITER"); do
ethtool -L veth0 tx 1
ethtool -L veth0 tx 2
# give linkwatch time to bring the qdisc back after the carrier toggle
sleep 2
a=$(sent); sleep 1; b=$(sent)
if [ $((b - a)) -lt 1000 ]; then
echo "STUCK after $i TX shrink/grow cycles: sent $a -> $b in 1s"
sleep 3
echo "3s later: sent $b -> $(sent), $(ip -br link show veth0)"
exit 1
fi
done
echo "OK: veth0 kept sending through $ITER TX shrink/grow cycles," \
"sent $(sent) pkts, requeues $(requeues)"
exit 0
drivers/net/veth.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/drivers/net/veth.c b/drivers/net/veth.c
index a61842b2585b..62a60a033675 100644
--- a/drivers/net/veth.c
+++ b/drivers/net/veth.c
@@ -984,7 +984,7 @@ static int veth_poll(struct napi_struct *napi, int budget)
/* NAPI functions as RCU section */
peer_dev = rcu_dereference_check(priv->peer, rcu_read_lock_bh_held());
- peer_txq = (peer_dev && queue_idx < peer_dev->real_num_tx_queues) ?
+ peer_txq = (peer_dev && queue_idx < peer_dev->num_tx_queues) ?
netdev_get_tx_queue(peer_dev, queue_idx) : NULL;
xdp_set_return_frame_no_direct();
--
2.43.0
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [PATCH net 2/2] veth: wake the peer txq when deleting NAPI
2026-11-13 10:46 [PATCH net 0/2] veth: fix txqs that are never woken after backpressure Simon Schippers
2026-11-13 10:46 ` [PATCH net 1/2] veth: wake a stopped peer txq beyond real_num_tx_queues Simon Schippers
@ 2026-11-13 10:46 ` Simon Schippers
1 sibling, 0 replies; 4+ messages in thread
From: Simon Schippers @ 2026-11-13 10:46 UTC (permalink / raw)
To: netdev
Cc: Andrew Lunn, David S . Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Jesper Dangaard Brouer, Toshiaki Makita,
Jonas Köppeler, Alexei Starovoitov, Daniel Borkmann,
John Fastabend, Stanislav Fomichev, simon.schippers, bpf,
linux-kernel
Since commit dc82a33297fc ("veth: apply qdisc backpressure on full
ptr_ring to reduce TX drops"), only the peer's veth_poll() wakes a txq
that veth_xmit() stopped on a full ptr_ring.
If veth_xmit() stops the txq after the last poll before napi_disable(),
its __veth_xdp_flush() can no longer schedule NAPI. Nothing wakes the
txq, also not after NAPI is added back, so the device stops transmitting
for good. This can happen whenever the peer deletes its NAPI: when it
goes down, turns GRO off, detaches XDP or reduces its RX channels.
Wake the peer txq in veth_napi_del_range() once NAPI is deleted, also
when the peer is down, as opening it does not restart its queues.
Reproduced in virtme-ng with a pktgen flood while toggling GRO on the
peer, with a udelay() before netif_tx_stop_queue() to widen the race:
the txq got stuck within four toggles without this patch, and not in 300
with it. Without the udelay() it was not hit in 1200 toggles.
Fixes: dc82a33297fc ("veth: apply qdisc backpressure on full ptr_ring to reduce TX drops")
Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Simon Schippers <simon.schippers@tu-dortmund.de>
---
Notes:
Reproducer (needs >= 3 CPUs). The race only triggers reliably with the
debug-only hook below, which widens the window before
netif_tx_stop_queue(). Run as root:
# stuck within a few toggles without this patch, never with it
./repro-veth-napi-del-txq-stall.sh 300 2000
Debug hook (not for merging, indented so git am skips it):
diff --git a/drivers/net/veth.c b/drivers/net/veth.c
--- a/drivers/net/veth.c
+++ b/drivers/net/veth.c
@@ -29,6 +29,11 @@
#include <linux/net_tstamp.h>
#include <linux/skbuff_ref.h>
#include <net/page_pool/helpers.h>
+#include <linux/delay.h>
+
+/* REPRO ONLY, never to be committed */
+static unsigned int repro_delay_us;
+module_param(repro_delay_us, uint, 0644);
#define DRV_NAME "veth"
#define DRV_VERSION "1.0"
@@ -402,6 +407,8 @@ static netdev_tx_t veth_xmit(struct sk_buff *skb, struct net_device *dev)
__skb_push(skb, ETH_HLEN);
/* qdisc requeues the skb on the txq it points to */
skb_set_queue_mapping(skb, rxq);
+ if (READ_ONCE(repro_delay_us))
+ udelay(READ_ONCE(repro_delay_us)); /* REPRO: widen window */
netif_tx_stop_queue(txq);
/* Makes sure NAPI peer consumer runs. Consumer is responsible
* for starting txq again, until then ndo_start_xmit (this
repro-veth-napi-del-txq-stall.sh:
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
#
# Reproducer: a veth txq stopped on a full ptr_ring stays stopped after the
# peer's NAPI is deleted.
#
# veth0 (init netns, pfifo qdisc, pktgen queue_xmit flood) -> veth1 (netns rx,
# GRO on, threaded NAPI). Needs at least 3 CPUs. The ring fills, veth_xmit()
# returns NETDEV_TX_BUSY and stops veth0's txq until veth_poll() wakes it. GRO
# on veth1 is toggled off and on, which deletes and re-adds its NAPI. If the
# txq is stopped after the last poll's wake check, nothing wakes it and veth0
# stops sending for good.
#
# Usage: repro-veth-napi-del-txq-stall.sh [iterations] [delay_us]
#
# delay_us > 0 writes veth.repro_delay_us, a debug-only hook (not part of any
# patch) that udelay()s between the ring-full check and netif_tx_stop_queue()
# to widen the race window.
#
# RULES (default 50) nft rules in netns rx slow the consumer down.
#
# Exits 1 if veth0 got stuck, 0 if it survived all iterations, 2 if the ring
# never filled.
ITER=${1:-300}
DELAY=${2:-0}
RULES=${RULES:-50}
PG=/proc/net/pktgen
cleanup() {
echo stop > $PG/pgctrl 2>/dev/null
wait 2>/dev/null
ip link del veth0 2>/dev/null
ip netns del rx 2>/dev/null
}
trap cleanup EXIT
sent() { tc -s qdisc show dev veth0 | awk '/Sent/ { print $4; exit }'; }
requeues() { tc -s qdisc show dev veth0 | sed -n 's/.*requeues \([0-9]*\).*/\1/p' | head -1; }
rx() { ip netns exec rx "$@"; }
# Threaded NAPI pinned away from kpktgend_0 (CPU 0), so the consumer can poll
# while the producer is between the ring-full check and the txq stop
gro_on() {
rx ethtool -K veth1 gro on
rx sh -c 'echo 1 > /sys/class/net/veth1/threaded'
taskset -pc 2 "$(pgrep napi/veth1)" >/dev/null
}
ip netns add rx
ip link add veth0 type veth peer name veth1 netns rx
ip addr add 10.0.0.1/24 dev veth0
rx ip addr add 10.0.0.2/24 dev veth1
ip link set veth0 up
rx ip link set veth1 up
tc qdisc replace dev veth0 root pfifo limit 1000
# pktgen skbs have no socket, so veth only takes them to the ring without TSO
ethtool -K veth0 tso off
gro_on
# Slow down the consumer so that the 256-entry ring fills up
rx nft add table inet slow
rx nft add chain inet slow in '{ type filter hook input priority 0; }'
for i in $(seq 1 "$RULES"); do echo "add rule inet slow in meta mark $i drop"; done | rx nft -f -
[ "$DELAY" -gt 0 ] && echo "$DELAY" > /sys/module/veth/parameters/repro_delay_us
echo "rem_device_all" > $PG/kpktgend_0
echo "add_device veth0" > $PG/kpktgend_0
for c in "count 0" "pkt_size 60" "delay 0" "clone_skb 0" \
"xmit_mode queue_xmit" "dst 10.0.0.2" "udp_dst_min 9" "udp_dst_max 9" \
"dst_mac $(rx cat /sys/class/net/veth1/address)"; do
echo "$c" > $PG/veth0
done
echo start > $PG/pgctrl &
sleep 1
echo "kernel $(uname -r), iterations $ITER, repro_delay_us $DELAY"
echo "after 1s flood: sent $(sent) pkts, requeues $(requeues)"
if [ "$(requeues)" = 0 ]; then
echo "ring never filled, raise RULES"
exit 2
fi
for i in $(seq 1 "$ITER"); do
rx ethtool -K veth1 gro off
a=$(sent); sleep 0.3; b=$(sent)
if [ "$a" = "$b" ]; then
echo "STUCK after $i GRO toggles: veth0 qdisc sent nothing in 0.3s"
gro_on
sleep 1
echo "still stuck with GRO back on: sent $a -> $(sent)"
echo stop > $PG/pgctrl; wait
sleep 0.5
ping -c 3 -W 1 10.0.0.2 >/dev/null 2>&1 &&
echo "ping: ok" || echo "ping 10.0.0.2: fails"
tc -s qdisc show dev veth0
exit 1
fi
gro_on
sleep 0.05
done
echo "OK: veth0 kept sending through $ITER GRO toggles," \
"sent $(sent) pkts, requeues $(requeues)"
exit 0
drivers/net/veth.c | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/drivers/net/veth.c b/drivers/net/veth.c
index 62a60a033675..faa837acea7b 100644
--- a/drivers/net/veth.c
+++ b/drivers/net/veth.c
@@ -1087,6 +1087,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++) {
@@ -1099,11 +1100,17 @@ static void veth_napi_del_range(struct net_device *dev, int start, int end)
}
synchronize_net();
+ peer = rtnl_dereference(priv->peer);
+
for (i = start; i < end; i++) {
struct veth_rq *rq = &priv->rq[i];
rq->rx_notify_masked = false;
ptr_ring_cleanup(&rq->xdp_ring, veth_ptr_free);
+
+ /* veth_poll() can no longer wake a txq stopped on a full ring */
+ if (peer && i < peer->num_tx_queues)
+ netif_wake_subqueue(peer, i);
}
for (i = start; i < end; i++) {
--
2.43.0
^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [PATCH net 1/2] veth: wake a stopped peer txq beyond real_num_tx_queues
2026-11-13 10:46 ` [PATCH net 1/2] veth: wake a stopped peer txq beyond real_num_tx_queues Simon Schippers
@ 2026-11-14 10:47 ` sashiko-bot
0 siblings, 0 replies; 4+ messages in thread
From: sashiko-bot @ 2026-11-14 10:47 UTC (permalink / raw)
To: Simon Schippers; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 1 potential
issue to consider:
- [Low] veth: Fixes trailer cites dc82a33297fc instead of 08f566e8f83b
--
commit 4511ff49371a3a9f93211f5b51b7a6404ecb1f22
Author: Simon Schippers <simon.schippers@tu-dortmund.de>
> veth: wake a stopped peer txq beyond real_num_tx_queues
>
> Since commit dc82a33297fc ("veth: apply qdisc backpressure on full
> ptr_ring to reduce TX drops"), only the peer's veth_poll() wakes a txq
> that veth_xmit() stopped on a full ptr_ring. veth_poll() skips txqs at
> or above the peer's real_num_tx_queues.
>
> If the peer shrinks its TX channels while such a txq is stopped, nothing
> wakes it. netif_set_real_num_tx_queues() keeps its state, so once the
> channels are grown back, its first skb is requeued forever. With a single
> root qdisc this blocks all transmission of the device.
>
> Look the txq up below num_tx_queues instead. Waking a txq that is not in
> use is harmless.
>
> Reproduced in virtme-ng with a UDP flood from veth0 into veth1, which has
> GRO, while switching veth0 between one and two TX channels: without this
> patch veth0 stopped transmitting within 11 cycles in 3 of 3 runs, with
> it veth0 kept transmitting through 30 cycles in 3 of 3 runs.
>
> Fixes: dc82a33297fc ("veth: apply qdisc backpressure on full ptr_ring to reduce TX drops")
> Assisted-by: Claude:claude-opus-5-5
> Signed-off-by: Simon Schippers <simon.schippers@tu-dortmund.de>
[Severity: Low]
Should the Fixes: tag reference 08f566e8f83b ("veth: fix OOB txq access
in veth_poll() with asymmetric queue counts") instead?
Commit dc82a33297fc ("veth: apply qdisc backpressure on full ptr_ring to
reduce TX drops") looked up peer_txq unconditionally via
netdev_get_tx_queue(peer_dev, queue_idx). The queue_idx <
peer_dev->real_num_tx_queues check that skips txqs above
real_num_tx_queues in veth_poll() was introduced in 08f566e8f83b ("veth:
fix OOB txq access in veth_poll() with asymmetric queue counts").
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261113104648.225983-1-simon.schippers@tu-dortmund.de?part=1
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-11-14 10:47 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-13 10:46 [PATCH net 0/2] veth: fix txqs that are never woken after backpressure Simon Schippers
2026-11-13 10:46 ` [PATCH net 1/2] veth: wake a stopped peer txq beyond real_num_tx_queues Simon Schippers
2026-11-14 10:47 ` sashiko-bot
2026-11-13 10:46 ` [PATCH net 2/2] veth: wake the peer txq when deleting NAPI Simon Schippers
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.