* [PATCH net-next v13 1/5] tun/tap: add IFF_BACKPRESSURE flag
2026-07-30 21:36 [PATCH net-next v13 0/5] tun/tap & vhost-net: apply qdisc backpressure on full ptr_ring to reduce TX drops Simon Schippers
@ 2026-07-30 21:36 ` Simon Schippers
2026-07-31 21:40 ` sashiko-bot
2026-07-30 21:36 ` [PATCH net-next v13 2/5] tun/tap: add ptr_ring consume helper with netdev queue wakeup Simon Schippers
` (3 subsequent siblings)
4 siblings, 1 reply; 8+ messages in thread
From: Simon Schippers @ 2026-07-30 21:36 UTC (permalink / raw)
To: willemdebruijn.kernel, jasowangio, jasowang, andrew+netdev, davem,
edumazet, kuba, pabeni, horms, mst, eperezma, leiyang, stephen,
jon, brett, corbet, skhan, tim.gebauer, simon.schippers, netdev,
linux-doc, linux-kernel, kvm, virtualization
Add the IFF_BACKPRESSURE flag to the UAPI header and to TUN_FEATURES so
that it can be set via TUNSETIFF. The flag has no effect yet, it is the
gate for the qdisc backpressure logic added by the following patches.
Keeping the flag and its users in separate patches would either leave a
window where backpressure is unconditional, or make the gate a later
add-on. Adding the flag first lets every following patch be a no-op
unless it is set.
Signed-off-by: Simon Schippers <simon.schippers@tu-dortmund.de>
---
drivers/net/tun.c | 3 ++-
include/uapi/linux/if_tun.h | 4 ++++
tools/include/uapi/linux/if_tun.h | 4 ++++
3 files changed, 10 insertions(+), 1 deletion(-)
diff --git a/drivers/net/tun.c b/drivers/net/tun.c
index 51e80000bd0e..ea8573efb848 100644
--- a/drivers/net/tun.c
+++ b/drivers/net/tun.c
@@ -98,7 +98,8 @@ static void tun_default_link_ksettings(struct net_device *dev,
#define TUN_FASYNC IFF_ATTACH_QUEUE
#define TUN_FEATURES (IFF_NO_PI | IFF_ONE_QUEUE | IFF_VNET_HDR | \
- IFF_MULTI_QUEUE | IFF_NAPI | IFF_NAPI_FRAGS)
+ IFF_MULTI_QUEUE | IFF_NAPI | IFF_NAPI_FRAGS | \
+ IFF_BACKPRESSURE)
#define GOODCOPY_LEN 128
diff --git a/include/uapi/linux/if_tun.h b/include/uapi/linux/if_tun.h
index 79d53c7a1ebd..a0ddc50a7534 100644
--- a/include/uapi/linux/if_tun.h
+++ b/include/uapi/linux/if_tun.h
@@ -69,6 +69,10 @@
#define IFF_NAPI_FRAGS 0x0020
/* Used in TUNSETIFF to bring up tun/tap without carrier */
#define IFF_NO_CARRIER 0x0040
+/* Stop the queue instead of dropping when the internal ring is full, so an
+ * attached qdisc applies backpressure instead of being bypassed.
+ */
+#define IFF_BACKPRESSURE 0x0080
#define IFF_NO_PI 0x1000
/* This flag has no real effect */
#define IFF_ONE_QUEUE 0x2000
diff --git a/tools/include/uapi/linux/if_tun.h b/tools/include/uapi/linux/if_tun.h
index 2ec07de1d73b..2c85525704c1 100644
--- a/tools/include/uapi/linux/if_tun.h
+++ b/tools/include/uapi/linux/if_tun.h
@@ -67,6 +67,10 @@
#define IFF_TAP 0x0002
#define IFF_NAPI 0x0010
#define IFF_NAPI_FRAGS 0x0020
+/* Stop the queue instead of dropping when the internal ring is full, so an
+ * attached qdisc applies backpressure instead of being bypassed.
+ */
+#define IFF_BACKPRESSURE 0x0080
#define IFF_NO_PI 0x1000
/* This flag has no real effect */
#define IFF_ONE_QUEUE 0x2000
--
2.43.0
^ permalink raw reply related [flat|nested] 8+ messages in thread* Re: [PATCH net-next v13 1/5] tun/tap: add IFF_BACKPRESSURE flag
2026-07-30 21:36 ` [PATCH net-next v13 1/5] tun/tap: add IFF_BACKPRESSURE flag Simon Schippers
@ 2026-07-31 21:40 ` sashiko-bot
0 siblings, 0 replies; 8+ messages in thread
From: sashiko-bot @ 2026-07-31 21:40 UTC (permalink / raw)
To: Simon Schippers; +Cc: kvm
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] Premature UAPI exposure of `IFF_BACKPRESSURE` flag breaks feature detection.
--
--- Patch [1]: [PATCH net-next v13 1/5] tun/tap: add IFF_BACKPRESSURE flag ---
commit 43e4cc9000a7c476758fd2aa10db82e86b3c41ca
Author: Simon Schippers <simon.schippers@tu-dortmund.de>
tun/tap: add IFF_BACKPRESSURE flag
This commit adds the IFF_BACKPRESSURE flag to the tun/tap UAPI headers and
enables it in TUN_FEATURES. The flag is intended to gate qdisc backpressure
logic added in subsequent patches.
> diff --git a/drivers/net/tun.c b/drivers/net/tun.c
> --- a/drivers/net/tun.c
> +++ b/drivers/net/tun.c
[ ... ]
> @@ -98,7 +98,8 @@ static void tun_default_link_ksettings(struct net_device *dev,
> #define TUN_FASYNC IFF_ATTACH_QUEUE
>
> #define TUN_FEATURES (IFF_NO_PI | IFF_ONE_QUEUE | IFF_VNET_HDR | \
> - IFF_MULTI_QUEUE | IFF_NAPI | IFF_NAPI_FRAGS)
> + IFF_MULTI_QUEUE | IFF_NAPI | IFF_NAPI_FRAGS | \
> + IFF_BACKPRESSURE)
[Severity: Medium]
Does adding this flag to TUN_FEATURES before the implementation is present
break userspace feature detection during a git bisect?
If userspace calls ioctl(TUNGETFEATURES) at this specific commit, it will
see IFF_BACKPRESSURE as supported and might set it via TUNSETIFF. However,
because the actual backpressure logic is delegated to later patches in the
series, the kernel will silently drop packets instead of exerting
backpressure.
Would it be safer to introduce the UAPI flag definitions early in the
series, but wait to add IFF_BACKPRESSURE to TUN_FEATURES until the final
patch when the implementation is fully in place?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260730213639.726381-1-simon.schippers@tu-dortmund.de?part=1
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH net-next v13 2/5] tun/tap: add ptr_ring consume helper with netdev queue wakeup
2026-07-30 21:36 [PATCH net-next v13 0/5] tun/tap & vhost-net: apply qdisc backpressure on full ptr_ring to reduce TX drops Simon Schippers
2026-07-30 21:36 ` [PATCH net-next v13 1/5] tun/tap: add IFF_BACKPRESSURE flag Simon Schippers
@ 2026-07-30 21:36 ` Simon Schippers
2026-07-31 21:40 ` sashiko-bot
2026-07-30 21:36 ` [PATCH net-next v13 3/5] vhost-net: wake queue of tun/tap after ptr_ring consume Simon Schippers
` (2 subsequent siblings)
4 siblings, 1 reply; 8+ messages in thread
From: Simon Schippers @ 2026-07-30 21:36 UTC (permalink / raw)
To: willemdebruijn.kernel, jasowangio, jasowang, andrew+netdev, davem,
edumazet, kuba, pabeni, horms, mst, eperezma, leiyang, stephen,
jon, brett, corbet, skhan, tim.gebauer, simon.schippers, netdev,
linux-doc, linux-kernel, kvm, virtualization
Introduce tun_ring_consume() that wraps ptr_ring_consume() and calls
__tun_wake_queue(). The latter wakes the stopped netdev subqueue once
half of the ring capacity has been consumed, tracked via the new
cons_cnt field in tun_file. As a safety net, the queue is also woken on
the last consumed entry if it leaves the ring empty. The point is to
allow the queue to be stopped when it gets full, which is required for
traffic shaping, implemented by the following "stop tail-drop when
IFF_BACKPRESSURE is set".
__tun_wake_queue() returns early unless IFF_BACKPRESSURE is set, so for a
tun/tap device that does not opt in only the added check on the consume
path remains.
Some implementation details:
- tun_ring_recv() replaces ptr_ring_consume() with tun_ring_consume()
to properly wake the queue.
- __tun_detach() locks the tx_ring.consumer_lock to avoid races with
the consumer on the queue_index.
- The ptr_ring_consume() call in tun_queue_purge() is not replaced with
tun_ring_consume(). Instead, within the same tx_ring.consumer_lock
in __tun_detach(), the netdev queue is woken for the ntfile taking
it over, to avoid a possible stall. This does not matter for
tun_detach_all(), as it is called during device teardown and no tfile
takes over any queue. The queue is only woken if the ring of
the ntfile is empty, as otherwise the consumer wakes it after
consuming the remaining entries.
- Ensure detached queues are woken on re-attach by calling the new
tun_force_wake_queue() helper from tun_attach(), and reuse it across
the existing wake paths. Unlike __tun_wake_queue() it ignores
IFF_BACKPRESSURE, so a queue can not stay stopped after the flag is
cleared.
- The aforementioned upcoming patch explains the pairing of the smp_mb()
of __tun_wake_queue().
Co-developed-by: Tim Gebauer <tim.gebauer@tu-dortmund.de>
Signed-off-by: Tim Gebauer <tim.gebauer@tu-dortmund.de>
Signed-off-by: Simon Schippers <simon.schippers@tu-dortmund.de>
---
drivers/net/tun.c | 77 ++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 73 insertions(+), 4 deletions(-)
diff --git a/drivers/net/tun.c b/drivers/net/tun.c
index ea8573efb848..d9db6d2e4c56 100644
--- a/drivers/net/tun.c
+++ b/drivers/net/tun.c
@@ -146,6 +146,8 @@ struct tun_file {
struct list_head next;
struct tun_struct *detached;
struct ptr_ring tx_ring;
+ /* Protected by tx_ring.consumer_lock */
+ int cons_cnt;
struct xdp_rxq_info xdp_rxq;
};
@@ -589,8 +591,16 @@ static void __tun_detach(struct tun_file *tfile, bool clean)
rcu_assign_pointer(tun->tfiles[index],
tun->tfiles[tun->numqueues - 1]);
ntfile = rtnl_dereference(tun->tfiles[index]);
+ spin_lock(&ntfile->tx_ring.consumer_lock);
ntfile->queue_index = index;
ntfile->xdp_rxq.queue_index = index;
+ ntfile->cons_cnt = 0;
+ /* If the ring is not empty, the consumer wakes the queue
+ * after consuming the remaining entries.
+ */
+ if (__ptr_ring_empty(&ntfile->tx_ring))
+ netif_wake_subqueue(tun->dev, index);
+ spin_unlock(&ntfile->tx_ring.consumer_lock);
rcu_assign_pointer(tun->tfiles[tun->numqueues - 1],
NULL);
@@ -688,6 +698,20 @@ static void tun_detach_all(struct net_device *dev)
module_put(THIS_MODULE);
}
+static void tun_force_wake_queue(struct tun_struct *tun,
+ struct tun_file *tfile)
+{
+ /* Ensure that the producer can not stop the
+ * queue concurrently by taking locks.
+ */
+ spin_lock_bh(&tfile->tx_ring.consumer_lock);
+ spin_lock(&tfile->tx_ring.producer_lock);
+ netif_wake_subqueue(tun->dev, tfile->queue_index);
+ tfile->cons_cnt = 0;
+ spin_unlock(&tfile->tx_ring.producer_lock);
+ spin_unlock_bh(&tfile->tx_ring.consumer_lock);
+}
+
static int tun_attach(struct tun_struct *tun, struct file *file,
bool skip_filter, bool napi, bool napi_frags,
bool publish_tun)
@@ -733,6 +757,7 @@ static int tun_attach(struct tun_struct *tun, struct file *file,
tfile->queue_index = tun->numqueues;
tfile->socket.sk->sk_shutdown &= ~RCV_SHUTDOWN;
+ tun_force_wake_queue(tun, tfile);
if (tfile->detached) {
/* Re-attach detached tfile, updating XDP queue_index */
@@ -2117,13 +2142,50 @@ static ssize_t tun_put_user(struct tun_struct *tun,
return total;
}
-static void *tun_ring_recv(struct tun_file *tfile, int noblock, int *err)
+/* Callers must hold ring.consumer_lock */
+static void __tun_wake_queue(struct tun_struct *tun,
+ struct tun_file *tfile, int consumed)
+{
+ struct netdev_queue *txq;
+
+ if (!(tun->flags & IFF_BACKPRESSURE))
+ return;
+
+ txq = netdev_get_tx_queue(tun->dev, tfile->queue_index);
+
+ /* Paired with smp_mb__after_atomic() in tun_net_xmit() */
+ smp_mb();
+ if (netif_tx_queue_stopped(txq)) {
+ tfile->cons_cnt += consumed;
+ if (tfile->cons_cnt >= tfile->tx_ring.size / 2 ||
+ __ptr_ring_empty(&tfile->tx_ring)) {
+ netif_tx_wake_queue(txq);
+ tfile->cons_cnt = 0;
+ }
+ }
+}
+
+static void *tun_ring_consume(struct tun_struct *tun, struct tun_file *tfile)
+{
+ void *ptr;
+
+ spin_lock(&tfile->tx_ring.consumer_lock);
+ ptr = __ptr_ring_consume(&tfile->tx_ring);
+ if (ptr)
+ __tun_wake_queue(tun, tfile, 1);
+
+ spin_unlock(&tfile->tx_ring.consumer_lock);
+ return ptr;
+}
+
+static void *tun_ring_recv(struct tun_struct *tun, struct tun_file *tfile,
+ int noblock, int *err)
{
DECLARE_WAITQUEUE(wait, current);
void *ptr = NULL;
int error = 0;
- ptr = ptr_ring_consume(&tfile->tx_ring);
+ ptr = tun_ring_consume(tun, tfile);
if (ptr)
goto out;
if (noblock) {
@@ -2135,7 +2197,7 @@ static void *tun_ring_recv(struct tun_file *tfile, int noblock, int *err)
while (1) {
set_current_state(TASK_INTERRUPTIBLE);
- ptr = ptr_ring_consume(&tfile->tx_ring);
+ ptr = tun_ring_consume(tun, tfile);
if (ptr)
break;
if (signal_pending(current)) {
@@ -2172,7 +2234,7 @@ static ssize_t tun_do_read(struct tun_struct *tun, struct tun_file *tfile,
if (!ptr) {
/* Read frames from ring */
- ptr = tun_ring_recv(tfile, noblock, &err);
+ ptr = tun_ring_recv(tun, tfile, noblock, &err);
if (!ptr)
return err;
}
@@ -3631,6 +3693,13 @@ static int tun_queue_resize(struct tun_struct *tun)
dev->tx_queue_len, GFP_KERNEL,
tun_ptr_free);
+ if (!ret) {
+ for (i = 0; i < tun->numqueues; i++) {
+ tfile = rtnl_dereference(tun->tfiles[i]);
+ tun_force_wake_queue(tun, tfile);
+ }
+ }
+
kfree(rings);
return ret;
}
--
2.43.0
^ permalink raw reply related [flat|nested] 8+ messages in thread* Re: [PATCH net-next v13 2/5] tun/tap: add ptr_ring consume helper with netdev queue wakeup
2026-07-30 21:36 ` [PATCH net-next v13 2/5] tun/tap: add ptr_ring consume helper with netdev queue wakeup Simon Schippers
@ 2026-07-31 21:40 ` sashiko-bot
0 siblings, 0 replies; 8+ messages in thread
From: sashiko-bot @ 2026-07-31 21:40 UTC (permalink / raw)
To: Simon Schippers; +Cc: kvm
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
New issues:
- [High] vhost-net bypasses the new queue wakeup logic (`tun_ring_consume`), leading to a permanent TX queue stall when IFF_BACKPRESSURE is used.
Pre-existing issues:
- [High] A structural flaw in `tun_set_iff` allows changing a non-multiqueue persistent TUN device into a multiqueue device, leading to out-of-bounds accesses on the `dev->_tx` array via the newly introduced `__tun_wake_queue`.
--
--- Patch [2]: [PATCH net-next v13 2/5] tun/tap: add ptr_ring consume helper with netdev queue wakeup ---
commit 537cb0b1726915d53bc505e79f9dac64d03106a9
Author: Simon Schippers <simon.schippers@tu-dortmund.de>
tun/tap: add ptr_ring consume helper with netdev queue wakeup
Introduce tun_ring_consume() that wraps ptr_ring_consume() and calls
__tun_wake_queue(). The latter wakes the stopped netdev subqueue once
half of the ring capacity has been consumed, tracked via the new
cons_cnt field in tun_file.
> diff --git a/drivers/net/tun.c b/drivers/net/tun.c
> index ea8573efb8489..d9db6d2e4c56a 100644
> --- a/drivers/net/tun.c
> +++ b/drivers/net/tun.c
[ ... ]
> @@ -2117,13 +2142,50 @@ static ssize_t tun_put_user(struct tun_struct *tun,
> return total;
> }
>
> -static void *tun_ring_recv(struct tun_file *tfile, int noblock, int *err)
> +/* Callers must hold ring.consumer_lock */
> +static void __tun_wake_queue(struct tun_struct *tun,
> + struct tun_file *tfile, int consumed)
> +{
> + struct netdev_queue *txq;
> +
> + if (!(tun->flags & IFF_BACKPRESSURE))
> + return;
> +
> + txq = netdev_get_tx_queue(tun->dev, tfile->queue_index);
[Severity: High]
This is a pre-existing issue, but can this lead to an out-of-bounds access
on the dev->_tx array?
If a persistent TUN device is initially created without IFF_MULTI_QUEUE, the
underlying net_device is allocated with only 1 TX queue. If the file
descriptor is closed and TUNSETIFF is called again with IFF_MULTI_QUEUE
enabled, tun_set_iff() allows the change but the net_device still only has
a single queue.
When a second file descriptor is attached, it receives a queue_index of 1.
Will this cause netdev_get_tx_queue() to access dev->_tx[1], resulting in
a heap out-of-bounds access when evaluated here?
> +
> + /* Paired with smp_mb__after_atomic() in tun_net_xmit() */
> + smp_mb();
> + if (netif_tx_queue_stopped(txq)) {
> + tfile->cons_cnt += consumed;
> + if (tfile->cons_cnt >= tfile->tx_ring.size / 2 ||
> + __ptr_ring_empty(&tfile->tx_ring)) {
> + netif_tx_wake_queue(txq);
> + tfile->cons_cnt = 0;
> + }
> + }
> +}
> +
> +static void *tun_ring_consume(struct tun_struct *tun, struct tun_file *tfile)
> +{
> + void *ptr;
> +
> + spin_lock(&tfile->tx_ring.consumer_lock);
> + ptr = __ptr_ring_consume(&tfile->tx_ring);
> + if (ptr)
> + __tun_wake_queue(tun, tfile, 1);
[Severity: High]
Will this cause a permanent TX queue stall when IFF_BACKPRESSURE is used
with vhost-net?
vhost-net directly accesses the transmit ring via tun_get_tx_ring() and
consumes packets natively, completely bypassing tun_ring_recv() and
tun_ring_consume().
Since the queue wakeup logic is exclusively placed here inside
tun_ring_consume(), does this mean the netdev subqueue will never be woken
up when vhost-net is consuming the packets?
> +
> + spin_unlock(&tfile->tx_ring.consumer_lock);
> + return ptr;
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260730213639.726381-1-simon.schippers@tu-dortmund.de?part=2
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH net-next v13 3/5] vhost-net: wake queue of tun/tap after ptr_ring consume
2026-07-30 21:36 [PATCH net-next v13 0/5] tun/tap & vhost-net: apply qdisc backpressure on full ptr_ring to reduce TX drops Simon Schippers
2026-07-30 21:36 ` [PATCH net-next v13 1/5] tun/tap: add IFF_BACKPRESSURE flag Simon Schippers
2026-07-30 21:36 ` [PATCH net-next v13 2/5] tun/tap: add ptr_ring consume helper with netdev queue wakeup Simon Schippers
@ 2026-07-30 21:36 ` Simon Schippers
2026-07-30 21:36 ` [PATCH net-next v13 4/5] ptr_ring: move free-space check into separate helper Simon Schippers
2026-07-30 21:36 ` [PATCH net-next v13 5/5] tun/tap & vhost-net: stop tail-drop when IFF_BACKPRESSURE is set Simon Schippers
4 siblings, 0 replies; 8+ messages in thread
From: Simon Schippers @ 2026-07-30 21:36 UTC (permalink / raw)
To: willemdebruijn.kernel, jasowangio, jasowang, andrew+netdev, davem,
edumazet, kuba, pabeni, horms, mst, eperezma, leiyang, stephen,
jon, brett, corbet, skhan, tim.gebauer, simon.schippers, netdev,
linux-doc, linux-kernel, kvm, virtualization
Add tun_wake_queue() to tun.c and export it for use by vhost-net. The
function validates that the file belongs to a tun/tap device and that
the tfile exists, dereferences the tun_struct under RCU, and delegates
to __tun_wake_queue().
vhost_net_buf_produce() now calls tun_wake_queue() after a successful
batched consume of the ring to allow the netdev subqueue to be woken up.
The point is to allow the queue to be stopped when it gets full, which
is required for traffic shaping, implemented by the following
"stop tail-drop when IFF_BACKPRESSURE is set".
As __tun_wake_queue() returns early unless IFF_BACKPRESSURE is set, a
tun/tap device that does not opt in only pays for the added check.
Co-developed-by: Tim Gebauer <tim.gebauer@tu-dortmund.de>
Signed-off-by: Tim Gebauer <tim.gebauer@tu-dortmund.de>
Signed-off-by: Simon Schippers <simon.schippers@tu-dortmund.de>
---
drivers/net/tun.c | 23 +++++++++++++++++++++++
drivers/vhost/net.c | 21 +++++++++++++++------
include/linux/if_tun.h | 3 +++
3 files changed, 41 insertions(+), 6 deletions(-)
diff --git a/drivers/net/tun.c b/drivers/net/tun.c
index d9db6d2e4c56..04275dcac456 100644
--- a/drivers/net/tun.c
+++ b/drivers/net/tun.c
@@ -3808,6 +3808,29 @@ struct ptr_ring *tun_get_tx_ring(struct file *file)
}
EXPORT_SYMBOL_GPL(tun_get_tx_ring);
+/* Callers must hold ring.consumer_lock */
+void tun_wake_queue(struct file *file, int consumed)
+{
+ struct tun_file *tfile;
+ struct tun_struct *tun;
+
+ if (file->f_op != &tun_fops)
+ return;
+
+ tfile = file->private_data;
+ if (!tfile)
+ return;
+
+ rcu_read_lock();
+
+ tun = rcu_dereference(tfile->tun);
+ if (tun)
+ __tun_wake_queue(tun, tfile, consumed);
+
+ rcu_read_unlock();
+}
+EXPORT_SYMBOL_GPL(tun_wake_queue);
+
module_init(tun_init);
module_exit(tun_cleanup);
MODULE_DESCRIPTION(DRV_DESCRIPTION);
diff --git a/drivers/vhost/net.c b/drivers/vhost/net.c
index 6949b704166d..3e72b9c6af0c 100644
--- a/drivers/vhost/net.c
+++ b/drivers/vhost/net.c
@@ -176,13 +176,21 @@ static void *vhost_net_buf_consume(struct vhost_net_buf *rxq)
return ret;
}
-static int vhost_net_buf_produce(struct vhost_net_virtqueue *nvq)
+static int vhost_net_buf_produce(struct sock *sk,
+ struct vhost_net_virtqueue *nvq)
{
+ struct file *file = sk->sk_socket->file;
struct vhost_net_buf *rxq = &nvq->rxq;
rxq->head = 0;
- rxq->tail = ptr_ring_consume_batched(nvq->rx_ring, rxq->queue,
- VHOST_NET_BATCH);
+ spin_lock(&nvq->rx_ring->consumer_lock);
+ rxq->tail = __ptr_ring_consume_batched(nvq->rx_ring, rxq->queue,
+ VHOST_NET_BATCH);
+
+ if (rxq->tail)
+ tun_wake_queue(file, rxq->tail);
+
+ spin_unlock(&nvq->rx_ring->consumer_lock);
return rxq->tail;
}
@@ -209,14 +217,15 @@ static int vhost_net_buf_peek_len(void *ptr)
return __skb_array_len_with_tag(ptr);
}
-static int vhost_net_buf_peek(struct vhost_net_virtqueue *nvq)
+static int vhost_net_buf_peek(struct sock *sk,
+ struct vhost_net_virtqueue *nvq)
{
struct vhost_net_buf *rxq = &nvq->rxq;
if (!vhost_net_buf_is_empty(rxq))
goto out;
- if (!vhost_net_buf_produce(nvq))
+ if (!vhost_net_buf_produce(sk, nvq))
return 0;
out:
@@ -1004,7 +1013,7 @@ static int peek_head_len(struct vhost_net_virtqueue *rvq, struct sock *sk)
unsigned long flags;
if (rvq->rx_ring)
- return vhost_net_buf_peek(rvq);
+ return vhost_net_buf_peek(sk, rvq);
spin_lock_irqsave(&sk->sk_receive_queue.lock, flags);
head = skb_peek(&sk->sk_receive_queue);
diff --git a/include/linux/if_tun.h b/include/linux/if_tun.h
index 80166eb62f41..5f3e206c7a73 100644
--- a/include/linux/if_tun.h
+++ b/include/linux/if_tun.h
@@ -22,6 +22,7 @@ struct tun_msg_ctl {
#if defined(CONFIG_TUN) || defined(CONFIG_TUN_MODULE)
struct socket *tun_get_socket(struct file *);
struct ptr_ring *tun_get_tx_ring(struct file *file);
+void tun_wake_queue(struct file *file, int consumed);
static inline bool tun_is_xdp_frame(void *ptr)
{
@@ -55,6 +56,8 @@ static inline struct ptr_ring *tun_get_tx_ring(struct file *f)
return ERR_PTR(-EINVAL);
}
+static inline void tun_wake_queue(struct file *f, int consumed) {}
+
static inline bool tun_is_xdp_frame(void *ptr)
{
return false;
--
2.43.0
^ permalink raw reply related [flat|nested] 8+ messages in thread* [PATCH net-next v13 4/5] ptr_ring: move free-space check into separate helper
2026-07-30 21:36 [PATCH net-next v13 0/5] tun/tap & vhost-net: apply qdisc backpressure on full ptr_ring to reduce TX drops Simon Schippers
` (2 preceding siblings ...)
2026-07-30 21:36 ` [PATCH net-next v13 3/5] vhost-net: wake queue of tun/tap after ptr_ring consume Simon Schippers
@ 2026-07-30 21:36 ` Simon Schippers
2026-07-30 21:36 ` [PATCH net-next v13 5/5] tun/tap & vhost-net: stop tail-drop when IFF_BACKPRESSURE is set Simon Schippers
4 siblings, 0 replies; 8+ messages in thread
From: Simon Schippers @ 2026-07-30 21:36 UTC (permalink / raw)
To: willemdebruijn.kernel, jasowangio, jasowang, andrew+netdev, davem,
edumazet, kuba, pabeni, horms, mst, eperezma, leiyang, stephen,
jon, brett, corbet, skhan, tim.gebauer, simon.schippers, netdev,
linux-doc, linux-kernel, kvm, virtualization
This patch moves the check for available free space for a new entry into
a separate function. Existing callers that only check for a non-zero
return value are unaffected; __ptr_ring_produce() now returns -EINVAL
for a zero-size ring and -ENOSPC when full, whereas before both cases
returned -ENOSPC. The new helper allows callers to determine in advance
whether subsequent __ptr_ring_produce() calls will succeed. This
information can, for example, be used to temporarily stop producing until
__ptr_ring_check_produce() indicates that space is available again.
Co-developed-by: Tim Gebauer <tim.gebauer@tu-dortmund.de>
Signed-off-by: Tim Gebauer <tim.gebauer@tu-dortmund.de>
Acked-by: Michael S. Tsirkin <mst@redhat.com>
Signed-off-by: Simon Schippers <simon.schippers@tu-dortmund.de>
---
include/linux/ptr_ring.h | 20 ++++++++++++++++++--
1 file changed, 18 insertions(+), 2 deletions(-)
diff --git a/include/linux/ptr_ring.h b/include/linux/ptr_ring.h
index d2c3629bbe45..c95e891903f0 100644
--- a/include/linux/ptr_ring.h
+++ b/include/linux/ptr_ring.h
@@ -96,6 +96,20 @@ static inline bool ptr_ring_full_bh(struct ptr_ring *r)
return ret;
}
+/* Note: callers invoking this in a loop must use a compiler barrier,
+ * for example cpu_relax(). Callers must hold producer_lock.
+ */
+static inline int __ptr_ring_check_produce(struct ptr_ring *r)
+{
+ if (unlikely(!r->size))
+ return -EINVAL;
+
+ if (data_race(r->queue[r->producer]))
+ return -ENOSPC;
+
+ return 0;
+}
+
/* Note: callers invoking this in a loop must use a compiler barrier,
* for example cpu_relax(). Callers must hold producer_lock.
* Callers are responsible for making sure pointer that is being queued
@@ -103,8 +117,10 @@ static inline bool ptr_ring_full_bh(struct ptr_ring *r)
*/
static inline int __ptr_ring_produce(struct ptr_ring *r, void *ptr)
{
- if (unlikely(!r->size) || data_race(r->queue[r->producer]))
- return -ENOSPC;
+ int p = __ptr_ring_check_produce(r);
+
+ if (p)
+ return p;
/* Make sure the pointer we are storing points to a valid data. */
/* Pairs with the dependency ordering in __ptr_ring_consume. */
--
2.43.0
^ permalink raw reply related [flat|nested] 8+ messages in thread* [PATCH net-next v13 5/5] tun/tap & vhost-net: stop tail-drop when IFF_BACKPRESSURE is set
2026-07-30 21:36 [PATCH net-next v13 0/5] tun/tap & vhost-net: apply qdisc backpressure on full ptr_ring to reduce TX drops Simon Schippers
` (3 preceding siblings ...)
2026-07-30 21:36 ` [PATCH net-next v13 4/5] ptr_ring: move free-space check into separate helper Simon Schippers
@ 2026-07-30 21:36 ` Simon Schippers
4 siblings, 0 replies; 8+ messages in thread
From: Simon Schippers @ 2026-07-30 21:36 UTC (permalink / raw)
To: willemdebruijn.kernel, jasowangio, jasowang, andrew+netdev, davem,
edumazet, kuba, pabeni, horms, mst, eperezma, leiyang, stephen,
jon, brett, corbet, skhan, tim.gebauer, simon.schippers, netdev,
linux-doc, linux-kernel, kvm, virtualization
This commit prevents tail-drop when IFF_BACKPRESSURE is set, a qdisc is
present and the ptr_ring becomes full. Once the ring reaches capacity
after a produce attempt, the netdev queue is stopped instead of dropping
subsequent packets. Without the flag, or if no qdisc is present, the
previous tail-drop behavior is preserved.
The unconditional version of this behavior was reverted because it caused
a significant throughput drop in an IPv6 multicast testcase on Brett
Sheffield's librecast testbed: with 8 iperf3 TCP threads sending, the
throughput dropped from 13.5 Gbit/s to 9.13 Gbit/s. This is why the queue
stopping is now gated on IFF_BACKPRESSURE.
If producing an entry fails anyway due to a race, tun_net_xmit() drops
the packet. Such rare races are expected because LLTX is enabled and the
transmit path operates without the usual locking.
Since the flag can be cleared again by a later TUNSETIFF, tun_set_iff()
calls tun_force_wake_queue() for the attached tfiles, so that no queue
stays stopped without a consumer that would wake it.
The __tun_wake_queue() function of the consumer races with the producer
for waking/stopping the netdev queue, which could result in a stalled
queue. Therefore, an smp_mb__after_atomic() is introduced that pairs
with the smp_mb() of the consumer. It follows the principle of store
buffering described in tools/memory-model/Documentation/recipes.txt:
- The producer in tun_net_xmit() first sets __QUEUE_STATE_DRV_XOFF,
followed by an smp_mb__after_atomic() (= smp_mb()), and then reads the
ring with __ptr_ring_check_produce().
- The consumer in __tun_wake_queue() first writes zero to the ring in
__ptr_ring_consume(), followed by an smp_mb(), and then reads the queue
status with netif_tx_queue_stopped().
=> Following the aforementioned principle, it is impossible for the
producer to see a full ring (and therefore not wake the queue on the
re-check) while the consumer simultaneously fails to see a stopped
queue (and therefore also does not wake it).
The documentation in tuntap.rst is updated accordingly.
Benchmarks:
My own benchmarks show a slight regression in raw transmission performance
when using two sending threads. Packet loss also occurs only in the
two-thread sending case; no packet loss was observed with a single
sending thread.
Test setup:
AMD Ryzen 5 5600X at 4.3 GHz, 3200 MHz RAM, isolated QEMU threads;
Average over 50 runs @ 100,000,000 packets. SRSO and spectre v2
mitigations disabled.
Note for tap+vhost-net:
XDP drop program active in VM -> ~2.5x faster; slower for tap due to
more syscalls (high utilization of entry_SYSRETQ_unsafe_stack in perf)
+--------------------------+--------------+----------------+----------+
| 1 thread | Stock | Patched with | diff |
| sending | | fq_codel qdisc | |
+------------+-------------+--------------+----------------+----------+
| TAP | Received | 1.132 Mpps | 1.123 Mpps | -0.8% |
| +-------------+--------------+----------------+----------+
| | Lost/s | 3.765 Mpps | 0 pps | |
+------------+-------------+--------------+----------------+----------+
| TAP | Received | 3.857 Mpps | 3.901 Mpps | +1.1% |
| +-------------+--------------+----------------+----------+
| +vhost-net | Lost/s | 0.802 Mpps | 0 pps | |
+------------+-------------+--------------+----------------+----------+
+--------------------------+--------------+----------------+----------+
| 2 threads | Stock | Patched with | diff |
| sending | | fq_codel qdisc | |
+------------+-------------+--------------+----------------+----------+
| TAP | Received | 1.115 Mpps | 1.081 Mpps | -3.0% |
| +-------------+--------------+----------------+----------+
| | Lost/s | 8.490 Mpps | 391 pps | |
+------------+-------------+--------------+----------------+----------+
| TAP | Received | 3.664 Mpps | 3.555 Mpps | -3.0% |
| +-------------+--------------+----------------+----------+
| +vhost-net | Lost/s | 5.330 Mpps | 938 pps | |
+------------+-------------+--------------+----------------+----------+
Co-developed-by: Tim Gebauer <tim.gebauer@tu-dortmund.de>
Signed-off-by: Tim Gebauer <tim.gebauer@tu-dortmund.de>
Link: https://lore.kernel.org/netdev/akVnoOYQOrt8k-Gu@karahi.librecast.net/
Signed-off-by: Simon Schippers <simon.schippers@tu-dortmund.de>
---
Documentation/networking/tuntap.rst | 22 ++++++++++++++++++
drivers/net/tun.c | 36 +++++++++++++++++++++++++----
2 files changed, 53 insertions(+), 5 deletions(-)
diff --git a/Documentation/networking/tuntap.rst b/Documentation/networking/tuntap.rst
index 4d7087f727be..04155ef98e5b 100644
--- a/Documentation/networking/tuntap.rst
+++ b/Documentation/networking/tuntap.rst
@@ -206,6 +206,28 @@ enable is true we enable it, otherwise we disable it::
return ioctl(fd, TUNSETQUEUE, (void *)&ifr);
}
+3.4 qdisc backpressure
+----------------------
+
+IFF_BACKPRESSURE can be set to enable qdisc backpressure. Without it, TX
+drops occur when the internal ring buffer is full, so any attached qdisc
+is effectively bypassed and applications only learn about congestion
+through those drops.
+
+With it, the kernel stops instead, letting the qdisc hold and schedule
+packets, so its AQM, shaping and fairness actually apply. This helps
+protocols like TCP, which cut throughput in reaction to packet drops.
+With IFF_BACKPRESSURE, drops then only occur as a rare race. Backpressure
+requires a qdisc to be attached and has no effect with noqueue.
+
+The txqueuelen can be reduced alongside this flag to further shift
+buffering into the qdisc and reduce bufferbloat, but comes at possible
+performance cost.
+
+When running multiple network streams in parallel through a single
+TUN/TAP queue, the flag may reduce performance due to the extra overhead
+of the backpressure mechanism.
+
Universal TUN/TAP device driver Frequently Asked Question
=========================================================
diff --git a/drivers/net/tun.c b/drivers/net/tun.c
index 04275dcac456..5a927bbbda2f 100644
--- a/drivers/net/tun.c
+++ b/drivers/net/tun.c
@@ -1034,6 +1034,7 @@ static netdev_tx_t tun_net_xmit(struct sk_buff *skb, struct net_device *dev)
struct netdev_queue *queue;
struct tun_file *tfile;
int len = skb->len;
+ int ret;
rcu_read_lock();
tfile = rcu_dereference(tun->tfiles[txq]);
@@ -1088,13 +1089,34 @@ static netdev_tx_t tun_net_xmit(struct sk_buff *skb, struct net_device *dev)
nf_reset_ct(skb);
- if (ptr_ring_produce(&tfile->tx_ring, skb)) {
+ queue = netdev_get_tx_queue(dev, txq);
+
+ spin_lock(&tfile->tx_ring.producer_lock);
+ ret = __ptr_ring_produce(&tfile->tx_ring, skb);
+ if ((tun->flags & IFF_BACKPRESSURE) &&
+ !qdisc_txq_has_no_queue(queue) &&
+ __ptr_ring_check_produce(&tfile->tx_ring) == -ENOSPC) {
+ netif_tx_stop_queue(queue);
+ /* Paired with smp_mb() in __tun_wake_queue() */
+ smp_mb__after_atomic();
+ if (!__ptr_ring_check_produce(&tfile->tx_ring))
+ netif_tx_wake_queue(queue);
+ }
+ spin_unlock(&tfile->tx_ring.producer_lock);
+
+ if (ret) {
+ /* This should be a rare case if IFF_BACKPRESSURE is enabled and
+ * a qdisc is present, but can happen due to lltx.
+ * Since skb_tx_timestamp(), skb_orphan(),
+ * run_ebpf_filter() and pskb_trim() could have tinkered
+ * with the SKB, returning NETDEV_TX_BUSY is unsafe and
+ * we must drop instead.
+ */
drop_reason = SKB_DROP_REASON_FULL_RING;
goto drop;
}
/* dev->lltx requires to do our own update of trans_start */
- queue = netdev_get_tx_queue(dev, txq);
txq_trans_cond_update(queue);
/* Notify and wake up reader process */
@@ -2767,7 +2789,7 @@ static int tun_set_iff(struct net *net, struct file *file, struct ifreq *ifr)
struct tun_struct *tun;
struct tun_file *tfile = file->private_data;
struct net_device *dev;
- int err;
+ int err, i;
if (tfile->detached)
return -EINVAL;
@@ -2896,8 +2918,12 @@ static int tun_set_iff(struct net *net, struct file *file, struct ifreq *ifr)
/* Make sure persistent devices do not get stuck in
* xoff state.
*/
- if (netif_running(tun->dev))
- netif_tx_wake_all_queues(tun->dev);
+ if (netif_running(tun->dev)) {
+ for (i = 0; i < tun->numqueues; i++) {
+ tfile = rtnl_dereference(tun->tfiles[i]);
+ tun_force_wake_queue(tun, tfile);
+ }
+ }
strscpy(ifr->ifr_name, tun->dev->name);
return 0;
--
2.43.0
^ permalink raw reply related [flat|nested] 8+ messages in thread