* [PATCH net v3] tun: bound the NAPI receive queue
@ 2026-10-31 23:14 Antonio Quartulli
2026-11-01 2:08 ` Willem de Bruijn
2026-11-01 23:23 ` netdev-bot+sashiko
0 siblings, 2 replies; 4+ messages in thread
From: Antonio Quartulli @ 2026-10-31 23:14 UTC (permalink / raw)
To: willemdebruijn.kernel, jasowangio
Cc: Antonio Quartulli, andrew+netdev, davem, edumazet, kuba, pabeni,
netdev
With IFF_NAPI, tun_get_user() queues every written packet for NAPI on
an unbounded list. When the poll runs asynchronously (threaded NAPI)
and is slower than the writer, the queue grows until the system runs
out of memory. An unprivileged user can do this from its own user and
network namespaces, and the queued skbs are not charged to its memcg:
in a test VM, a uid 1000 process limited to 128 MB caused a global OOM
in ~6 seconds. The socket send buffer cannot bound this: tun sets it to
INT_MAX, and the build_skb() and XDP batch paths do not check it.
Drop packets once more than netdev_max_backlog of them are queued, like
enqueue_to_backlog() does, and count them as rx_dropped: before
allocating and copying them where possible, and again under the queue
lock before queueing. This covers write(), sendmsg() and the batches of
XDP buffers vhost-net passes to tun_sendmsg(), which share the queue.
Zerocopy buffers of packets dropped early are completed.
Reproducer, as an unprivileged user in a new user and network
namespace, without kernel modifications or fault injection:
ip tuntap add t0 mode tun; ip addr add 10.11.0.1/24 dev t0
ip link add d0 type dummy; ip addr add 10.20.0.1/24 dev d0
ip link set t0 up; ip link set d0 up; sysctl net.ipv4.ip_forward=1
install ~500 iptables FORWARD rules and a MASQUERADE rule on d0
open t0 with IFF_TUN | IFF_NO_PI | IFF_NAPI
echo 1 > /sys/class/net/t0/threaded
write() 1400-byte IPv4 packets for 10.20.0.2 in a loop
Tested with the reproducer in an x86-64 VM: memory use stays flat
and the forwarding rate is unchanged. The vhost-net paths are only
build-tested, and the kselftests have not been run yet.
Still open: the limit counts packets rather than bytes and applies per
queue, the NAPI poll splices the queue out while it runs so up to about
twice the limit can be in flight, and the queued skbs are still not
charged to the writer's memcg.
Found, fixed and tested with the help of an AI coding assistant
(Claude Code).
Fixes: 943170998b20 ("tun: enable NAPI for TUN/TAP driver")
Signed-off-by: Antonio Quartulli <antonio@mandelbit.com>
---
v3:
- Also bound the sendmsg() paths, which share the queue: zerocopy
buffers are completed when dropped early, and the batched XDP buffers
of tun_xdp_one() are covered too (Willem)
- Drop the IFF_NAPI_FRAGS exclusion: tun_get_user() never queues in
that mode (napi_gro_frags() is synchronous), so the early check only
fires if tun_xdp_one() filled the same queue, and dropping is fine
then too (Willem)
- Recheck under the queue lock before queueing (Willem)
- Compare with > like enqueue_to_backlog() (Willem)
- Keep the includes sorted (Willem)
- Add the reproducer and a testing statement to the commit message
v2: https://lore.kernel.org/netdev/20261030152540.1409884-1-antonio@mandelbit.com/
- Cc netdev and the maintainers
Notes:
- Affected since 943170998b20 (v4.15). Reproduced on mainline
7be4bc52fd9e; the code is unchanged in net/main 94cea342e891, which
this patch is based on and was build-tested (W=1) against.
- Mitigation: disallow unprivileged user namespaces or restrict
access to /dev/net/tun.
drivers/net/tun.c | 39 +++++++++++++++++++++++++++++++++++++++
1 file changed, 39 insertions(+)
diff --git a/drivers/net/tun.c b/drivers/net/tun.c
index 770b8fe085c3..df5f0cbd1b16 100644
--- a/drivers/net/tun.c
+++ b/drivers/net/tun.c
@@ -58,6 +58,7 @@
#include <linux/nsproxy.h>
#include <linux/virtio_net.h>
#include <linux/rcupdate.h>
+#include <net/hotdata.h>
#include <net/net_namespace.h>
#include <net/netns/generic.h>
#include <net/rtnetlink.h>
@@ -227,6 +228,15 @@ struct veth {
static void tun_flow_init(struct tun_struct *tun);
static void tun_flow_uninit(struct tun_struct *tun);
+/* In IFF_NAPI mode written packets wait in the tfile's sk_write_queue for the
+ * NAPI poll, which may run later in a kthread or ksoftirqd. Bound that queue
+ * like the per-CPU backlog of netif_rx().
+ */
+static bool tun_napi_queue_full(const struct sk_buff_head *queue)
+{
+ return skb_queue_len_lockless(queue) > READ_ONCE(net_hotdata.max_backlog);
+}
+
static int tun_napi_receive(struct napi_struct *napi, int budget)
{
struct tun_file *tfile = container_of(napi, struct tun_file, napi);
@@ -1856,6 +1866,20 @@ static ssize_t tun_get_user(struct tun_struct *tun, struct tun_file *tfile,
zerocopy = true;
}
+ /* Drop before allocating and copying when the NAPI queue is full. The
+ * check is repeated under the queue lock before queueing.
+ */
+ if (tfile->napi_enabled &&
+ unlikely(tun_napi_queue_full(&tfile->sk.sk_write_queue))) {
+ if (msg_control) {
+ struct ubuf_info *uarg = msg_control;
+
+ uarg->ops->complete(NULL, uarg, false);
+ }
+ dev_core_stats_rx_dropped_inc(tun->dev);
+ return total_len;
+ }
+
if (!frags && tun_can_build_skb(tun, tfile, len, noblock, zerocopy)) {
/* For the packet that is not easy to be processed
* (e.g gso or jumbo packet), we will do it at after
@@ -2045,6 +2069,14 @@ static ssize_t tun_get_user(struct tun_struct *tun, struct tun_file *tfile,
goto free_skb;
}
+ if (unlikely(tun_napi_queue_full(queue))) {
+ spin_unlock_bh(&queue->lock);
+ rcu_read_unlock();
+ kfree_skb_reason(skb, SKB_DROP_REASON_FULL_RING);
+ dev_core_stats_rx_dropped_inc(tun->dev);
+ return total_len;
+ }
+
__skb_queue_tail(queue, skb);
queue_len = skb_queue_len(queue);
spin_unlock(&queue->lock);
@@ -2649,6 +2681,13 @@ static int tun_xdp_one(struct tun_struct *tun,
return -EBUSY;
}
+ if (unlikely(tun_napi_queue_full(queue))) {
+ spin_unlock(&queue->lock);
+ kfree_skb_reason(skb, SKB_DROP_REASON_FULL_RING);
+ dev_core_stats_rx_dropped_inc(tun->dev);
+ return 0;
+ }
+
__skb_queue_tail(queue, skb);
spin_unlock(&queue->lock);
ret = 1;
--
2.56.0
^ permalink raw reply related [flat|nested] 4+ messages in thread* Re: [PATCH net v3] tun: bound the NAPI receive queue
2026-10-31 23:14 [PATCH net v3] tun: bound the NAPI receive queue Antonio Quartulli
@ 2026-11-01 2:08 ` Willem de Bruijn
2026-11-01 20:17 ` Antonio Quartulli
2026-11-01 23:23 ` netdev-bot+sashiko
1 sibling, 1 reply; 4+ messages in thread
From: Willem de Bruijn @ 2026-11-01 2:08 UTC (permalink / raw)
To: Antonio Quartulli, willemdebruijn.kernel, jasowangio
Cc: Antonio Quartulli, andrew+netdev, davem, edumazet, kuba, pabeni,
netdev
Antonio Quartulli wrote:
> With IFF_NAPI, tun_get_user() queues every written packet for NAPI on
> an unbounded list. When the poll runs asynchronously (threaded NAPI)
> and is slower than the writer, the queue grows until the system runs
> out of memory. An unprivileged user can do this from its own user and
> network namespaces, and the queued skbs are not charged to its memcg:
> in a test VM, a uid 1000 process limited to 128 MB caused a global OOM
> in ~6 seconds. The socket send buffer cannot bound this: tun sets it to
> INT_MAX, and the build_skb() and XDP batch paths do not check it.
>
> Drop packets once more than netdev_max_backlog of them are queued, like
> enqueue_to_backlog() does, and count them as rx_dropped: before
> allocating and copying them where possible, and again under the queue
> lock before queueing. This covers write(), sendmsg() and the batches of
> XDP buffers vhost-net passes to tun_sendmsg(), which share the queue.
> Zerocopy buffers of packets dropped early are completed.
>
> Reproducer, as an unprivileged user in a new user and network
> namespace, without kernel modifications or fault injection:
> ip tuntap add t0 mode tun; ip addr add 10.11.0.1/24 dev t0
> ip link add d0 type dummy; ip addr add 10.20.0.1/24 dev d0
> ip link set t0 up; ip link set d0 up; sysctl net.ipv4.ip_forward=1
> install ~500 iptables FORWARD rules and a MASQUERADE rule on d0
> open t0 with IFF_TUN | IFF_NO_PI | IFF_NAPI
> echo 1 > /sys/class/net/t0/threaded
> write() 1400-byte IPv4 packets for 10.20.0.2 in a loop
>
> Tested with the reproducer in an x86-64 VM: memory use stays flat
> and the forwarding rate is unchanged. The vhost-net paths are only
> build-tested, and the kselftests have not been run yet.
Let's make sure the tun selftests pass.
> Still open: the limit counts packets rather than bytes and applies per
> queue, the NAPI poll splices the queue out while it runs so up to about
> twice the limit can be in flight, and the queued skbs are still not
> charged to the writer's memcg.
>
> Found, fixed and tested with the help of an AI coding assistant
> (Claude Code).
>
> Fixes: 943170998b20 ("tun: enable NAPI for TUN/TAP driver")
> Signed-off-by: Antonio Quartulli <antonio@mandelbit.com>
> ---
> v3:
> - Also bound the sendmsg() paths, which share the queue: zerocopy
> buffers are completed when dropped early, and the batched XDP buffers
> of tun_xdp_one() are covered too (Willem)
> - Drop the IFF_NAPI_FRAGS exclusion: tun_get_user() never queues in
> that mode (napi_gro_frags() is synchronous), so the early check only
> fires if tun_xdp_one() filled the same queue, and dropping is fine
> then too (Willem)
> - Recheck under the queue lock before queueing (Willem)
> - Compare with > like enqueue_to_backlog() (Willem)
> - Keep the includes sorted (Willem)
> - Add the reproducer and a testing statement to the commit message
>
> v2: https://lore.kernel.org/netdev/20261030152540.1409884-1-antonio@mandelbit.com/
> - Cc netdev and the maintainers
>
> Notes:
> - Affected since 943170998b20 (v4.15). Reproduced on mainline
> 7be4bc52fd9e; the code is unchanged in net/main 94cea342e891, which
> this patch is based on and was build-tested (W=1) against.
> - Mitigation: disallow unprivileged user namespaces or restrict
> access to /dev/net/tun.
>
> drivers/net/tun.c | 39 +++++++++++++++++++++++++++++++++++++++
> 1 file changed, 39 insertions(+)
>
> diff --git a/drivers/net/tun.c b/drivers/net/tun.c
> index 770b8fe085c3..df5f0cbd1b16 100644
> --- a/drivers/net/tun.c
> +++ b/drivers/net/tun.c
> @@ -58,6 +58,7 @@
> #include <linux/nsproxy.h>
> #include <linux/virtio_net.h>
> #include <linux/rcupdate.h>
> +#include <net/hotdata.h>
> #include <net/net_namespace.h>
> #include <net/netns/generic.h>
> #include <net/rtnetlink.h>
> @@ -227,6 +228,15 @@ struct veth {
> static void tun_flow_init(struct tun_struct *tun);
> static void tun_flow_uninit(struct tun_struct *tun);
>
> +/* In IFF_NAPI mode written packets wait in the tfile's sk_write_queue for the
> + * NAPI poll, which may run later in a kthread or ksoftirqd. Bound that queue
> + * like the per-CPU backlog of netif_rx().
> + */
> +static bool tun_napi_queue_full(const struct sk_buff_head *queue)
> +{
> + return skb_queue_len_lockless(queue) > READ_ONCE(net_hotdata.max_backlog);
> +}
> +
> static int tun_napi_receive(struct napi_struct *napi, int budget)
> {
> struct tun_file *tfile = container_of(napi, struct tun_file, napi);
> @@ -1856,6 +1866,20 @@ static ssize_t tun_get_user(struct tun_struct *tun, struct tun_file *tfile,
> zerocopy = true;
> }
>
> + /* Drop before allocating and copying when the NAPI queue is full. The
> + * check is repeated under the queue lock before queueing.
> + */
> + if (tfile->napi_enabled &&
> + unlikely(tun_napi_queue_full(&tfile->sk.sk_write_queue))) {
> + if (msg_control) {
> + struct ubuf_info *uarg = msg_control;
> +
> + uarg->ops->complete(NULL, uarg, false);
> + }
> + dev_core_stats_rx_dropped_inc(tun->dev);
> + return total_len;
> + }
> +
> if (!frags && tun_can_build_skb(tun, tfile, len, noblock, zerocopy)) {
> /* For the packet that is not easy to be processed
> * (e.g gso or jumbo packet), we will do it at after
> @@ -2045,6 +2069,14 @@ static ssize_t tun_get_user(struct tun_struct *tun, struct tun_file *tfile,
> goto free_skb;
> }
>
> + if (unlikely(tun_napi_queue_full(queue))) {
> + spin_unlock_bh(&queue->lock);
> + rcu_read_unlock();
> + kfree_skb_reason(skb, SKB_DROP_REASON_FULL_RING);
> + dev_core_stats_rx_dropped_inc(tun->dev);
> + return total_len;
consider goto free_skb instead of open coding.
> + }
> +
> __skb_queue_tail(queue, skb);
> queue_len = skb_queue_len(queue);
> spin_unlock(&queue->lock);
> @@ -2649,6 +2681,13 @@ static int tun_xdp_one(struct tun_struct *tun,
> return -EBUSY;
> }
>
> + if (unlikely(tun_napi_queue_full(queue))) {
> + spin_unlock(&queue->lock);
> + kfree_skb_reason(skb, SKB_DROP_REASON_FULL_RING);
> + dev_core_stats_rx_dropped_inc(tun->dev);
> + return 0;
> + }
> +
> __skb_queue_tail(queue, skb);
> spin_unlock(&queue->lock);
> ret = 1;
> --
> 2.56.0
>
^ permalink raw reply [flat|nested] 4+ messages in thread* Re: [PATCH net v3] tun: bound the NAPI receive queue
2026-11-01 2:08 ` Willem de Bruijn
@ 2026-11-01 20:17 ` Antonio Quartulli
0 siblings, 0 replies; 4+ messages in thread
From: Antonio Quartulli @ 2026-11-01 20:17 UTC (permalink / raw)
To: Willem de Bruijn, jasowangio
Cc: andrew+netdev, davem, edumazet, kuba, pabeni, netdev
Hi,
On 01/11/2026 03:08, Willem de Bruijn wrote:
> Antonio Quartulli wrote:
>> With IFF_NAPI, tun_get_user() queues every written packet for NAPI on
>> an unbounded list. When the poll runs asynchronously (threaded NAPI)
>> and is slower than the writer, the queue grows until the system runs
>> out of memory. An unprivileged user can do this from its own user and
>> network namespaces, and the queued skbs are not charged to its memcg:
>> in a test VM, a uid 1000 process limited to 128 MB caused a global OOM
>> in ~6 seconds. The socket send buffer cannot bound this: tun sets it to
>> INT_MAX, and the build_skb() and XDP batch paths do not check it.
>>
>> Drop packets once more than netdev_max_backlog of them are queued, like
>> enqueue_to_backlog() does, and count them as rx_dropped: before
>> allocating and copying them where possible, and again under the queue
>> lock before queueing. This covers write(), sendmsg() and the batches of
>> XDP buffers vhost-net passes to tun_sendmsg(), which share the queue.
>> Zerocopy buffers of packets dropped early are completed.
>>
>> Reproducer, as an unprivileged user in a new user and network
>> namespace, without kernel modifications or fault injection:
>> ip tuntap add t0 mode tun; ip addr add 10.11.0.1/24 dev t0
>> ip link add d0 type dummy; ip addr add 10.20.0.1/24 dev d0
>> ip link set t0 up; ip link set d0 up; sysctl net.ipv4.ip_forward=1
>> install ~500 iptables FORWARD rules and a MASQUERADE rule on d0
>> open t0 with IFF_TUN | IFF_NO_PI | IFF_NAPI
>> echo 1 > /sys/class/net/t0/threaded
>> write() 1400-byte IPv4 packets for 10.20.0.2 in a loop
>>
>> Tested with the reproducer in an x86-64 VM: memory use stays flat
>> and the forwarding rate is unchanged. The vhost-net paths are only
>> build-tested, and the kselftests have not been run yet.
>
> Let's make sure the tun selftests pass.
I went through the tun selftest and it passed.
However, there was no coverage for vhost-net, so I took care of that
with an extra patch (it was actually useful because I caught a hitch in
the actual fix).
>
>> Still open: the limit counts packets rather than bytes and applies per
>> queue, the NAPI poll splices the queue out while it runs so up to about
>> twice the limit can be in flight, and the queued skbs are still not
>> charged to the writer's memcg.
>>
>> Found, fixed and tested with the help of an AI coding assistant
>> (Claude Code).
>>
>> Fixes: 943170998b20 ("tun: enable NAPI for TUN/TAP driver")
>> Signed-off-by: Antonio Quartulli <antonio@mandelbit.com>
>> ---
>> v3:
>> - Also bound the sendmsg() paths, which share the queue: zerocopy
>> buffers are completed when dropped early, and the batched XDP buffers
>> of tun_xdp_one() are covered too (Willem)
>> - Drop the IFF_NAPI_FRAGS exclusion: tun_get_user() never queues in
>> that mode (napi_gro_frags() is synchronous), so the early check only
>> fires if tun_xdp_one() filled the same queue, and dropping is fine
>> then too (Willem)
>> - Recheck under the queue lock before queueing (Willem)
>> - Compare with > like enqueue_to_backlog() (Willem)
>> - Keep the includes sorted (Willem)
>> - Add the reproducer and a testing statement to the commit message
>>
>> v2: https://lore.kernel.org/netdev/20261030152540.1409884-1-antonio@mandelbit.com/
>> - Cc netdev and the maintainers
>>
>> Notes:
>> - Affected since 943170998b20 (v4.15). Reproduced on mainline
>> 7be4bc52fd9e; the code is unchanged in net/main 94cea342e891, which
>> this patch is based on and was build-tested (W=1) against.
>> - Mitigation: disallow unprivileged user namespaces or restrict
>> access to /dev/net/tun.
>>
>> drivers/net/tun.c | 39 +++++++++++++++++++++++++++++++++++++++
>> 1 file changed, 39 insertions(+)
>>
>> diff --git a/drivers/net/tun.c b/drivers/net/tun.c
>> index 770b8fe085c3..df5f0cbd1b16 100644
>> --- a/drivers/net/tun.c
>> +++ b/drivers/net/tun.c
>> @@ -58,6 +58,7 @@
>> #include <linux/nsproxy.h>
>> #include <linux/virtio_net.h>
>> #include <linux/rcupdate.h>
>> +#include <net/hotdata.h>
>> #include <net/net_namespace.h>
>> #include <net/netns/generic.h>
>> #include <net/rtnetlink.h>
>> @@ -227,6 +228,15 @@ struct veth {
>> static void tun_flow_init(struct tun_struct *tun);
>> static void tun_flow_uninit(struct tun_struct *tun);
>>
>> +/* In IFF_NAPI mode written packets wait in the tfile's sk_write_queue for the
>> + * NAPI poll, which may run later in a kthread or ksoftirqd. Bound that queue
>> + * like the per-CPU backlog of netif_rx().
>> + */
>> +static bool tun_napi_queue_full(const struct sk_buff_head *queue)
>> +{
>> + return skb_queue_len_lockless(queue) > READ_ONCE(net_hotdata.max_backlog);
>> +}
>> +
>> static int tun_napi_receive(struct napi_struct *napi, int budget)
>> {
>> struct tun_file *tfile = container_of(napi, struct tun_file, napi);
>> @@ -1856,6 +1866,20 @@ static ssize_t tun_get_user(struct tun_struct *tun, struct tun_file *tfile,
>> zerocopy = true;
>> }
>>
>> + /* Drop before allocating and copying when the NAPI queue is full. The
>> + * check is repeated under the queue lock before queueing.
>> + */
>> + if (tfile->napi_enabled &&
>> + unlikely(tun_napi_queue_full(&tfile->sk.sk_write_queue))) {
>> + if (msg_control) {
>> + struct ubuf_info *uarg = msg_control;
>> +
>> + uarg->ops->complete(NULL, uarg, false);
>> + }
>> + dev_core_stats_rx_dropped_inc(tun->dev);
>> + return total_len;
>> + }
>> +
>> if (!frags && tun_can_build_skb(tun, tfile, len, noblock, zerocopy)) {
>> /* For the packet that is not easy to be processed
>> * (e.g gso or jumbo packet), we will do it at after
>> @@ -2045,6 +2069,14 @@ static ssize_t tun_get_user(struct tun_struct *tun, struct tun_file *tfile,
>> goto free_skb;
>> }
>>
>> + if (unlikely(tun_napi_queue_full(queue))) {
>> + spin_unlock_bh(&queue->lock);
>> + rcu_read_unlock();
>> + kfree_skb_reason(skb, SKB_DROP_REASON_FULL_RING);
>> + dev_core_stats_rx_dropped_inc(tun->dev);
>> + return total_len;
>
> consider goto free_skb instead of open coding.
ACK.
Regards,
>> + }
>> +
>> __skb_queue_tail(queue, skb);
>> queue_len = skb_queue_len(queue);
>> spin_unlock(&queue->lock);
>> @@ -2649,6 +2681,13 @@ static int tun_xdp_one(struct tun_struct *tun,
>> return -EBUSY;
>> }
>>
>> + if (unlikely(tun_napi_queue_full(queue))) {
>> + spin_unlock(&queue->lock);
>> + kfree_skb_reason(skb, SKB_DROP_REASON_FULL_RING);
>> + dev_core_stats_rx_dropped_inc(tun->dev);
>> + return 0;
>> + }
>> +
>> __skb_queue_tail(queue, skb);
>> spin_unlock(&queue->lock);
>> ret = 1;
>> --
>> 2.56.0
>>
>
--
Antonio Quartulli
CEO and Co-Founder
Mandelbit Srl
https://www.mandelbit.com
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH net v3] tun: bound the NAPI receive queue
2026-10-31 23:14 [PATCH net v3] tun: bound the NAPI receive queue Antonio Quartulli
2026-11-01 2:08 ` Willem de Bruijn
@ 2026-11-01 23:23 ` netdev-bot+sashiko
1 sibling, 0 replies; 4+ messages in thread
From: netdev-bot+sashiko @ 2026-11-01 23:23 UTC (permalink / raw)
To: antonio
Cc: willemdebruijn.kernel, jasowangio, andrew+netdev, davem, edumazet,
kuba, pabeni, netdev
Thank you for your contribution! Sashiko AI review found 2 potential
issue(s) to consider.
Critical: 0 · High: 1 · Medium: 1 · Low: 0
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [High] The new limit does not bound the IFF_NAPI queue in the case the
patch is meant to fix: a threaded NAPI poll on another CPU that is…
- [Medium] The new full-queue drop paths return before NAPI is scheduled,
which can leave queued skbs stuck.
--
tun: bound the NAPI receive queue
In IFF_NAPI mode, packets written with write() or sendmsg() wait on the
tfile's sk_write_queue for the NAPI poll. That queue has had no limit.
This patch drops packets once more than netdev_max_backlog are queued.
The check runs before allocation and again under the queue lock, and
each dropped packet is counted as rx_dropped.
> diff --git a/drivers/net/tun.c b/drivers/net/tun.c
> index dcec7311e6011..410efa8ce5d91 100644
> --- a/drivers/net/tun.c
> +++ b/drivers/net/tun.c
[ ... ]
> @@ -227,6 +228,15 @@ struct veth {
> static void tun_flow_init(struct tun_struct *tun);
> static void tun_flow_uninit(struct tun_struct *tun);
>
> +/* In IFF_NAPI mode written packets wait in the tfile's sk_write_queue for the
> + * NAPI poll, which may run later in a kthread or ksoftirqd. Bound that queue
> + * like the per-CPU backlog of netif_rx().
> + */
> +static bool tun_napi_queue_full(const struct sk_buff_head *queue)
> +{
> + return skb_queue_len_lockless(queue) > READ_ONCE(net_hotdata.max_backlog);
> +}
[Severity: High]
Does this bound the queue in the case the patch is meant to fix, where a
threaded NAPI poll on another CPU is slower than the writer?
tun_napi_queue_full() only looks at the length of sk_write_queue. But
tun_napi_receive() moves the whole queue out before it starts:
drivers/net/tun.c:tun_napi_receive() {
...
spin_lock(&queue->lock);
skb_queue_splice_tail_init(queue, &process_queue);
spin_unlock(&queue->lock);
while (received < budget && (skb = __skb_dequeue(&process_queue))) {
napi_gro_receive(napi, skb);
++received;
}
if (!skb_queue_empty(&process_queue)) {
spin_lock(&queue->lock);
skb_queue_splice(&process_queue, queue);
spin_unlock(&queue->lock);
}
...
}
While the poll works through process_queue, the qlen of sk_write_queue
is 0. Writers in tun_get_user() and tun_xdp_one() can therefore queue up
to max_backlog + 1 new skbs. The poll then splices the unprocessed
leftovers back in front of them.
If L skbs are queued and W skbs are written during the poll, the queue
holds (L - 64) + W afterwards. Since received == budget, tun_napi_poll()
does not call napi_complete_done(). The next poll splices everything out
again right away and the window reopens.
Doesn't that let the backlog grow by up to about max_backlog + 1 - 64
skbs per poll, for as long as the writer is faster than the poll?
The commit message says:
the NAPI poll splices the queue out while it runs so up to about
twice the limit can be in flight
That 2x bound holds for enqueue_to_backlog() and process_backlog() in
net/core/dev.c. There, leftovers stay in sd->process_queue, and
input_pkt_queue is only spliced over once process_queue is empty. tun
puts its leftovers back on the shared queue, so does the statement
apply here?
Would the leftovers need to stay off sk_write_queue, or be counted
against the limit, for the bound to hold?
> +
> static int tun_napi_receive(struct napi_struct *napi, int budget)
> {
> struct tun_file *tfile = container_of(napi, struct tun_file, napi);
> @@ -1856,6 +1866,20 @@ static ssize_t tun_get_user(struct tun_struct *tun, struct tun_file *tfile,
> zerocopy = true;
> }
>
> + /* Drop before allocating and copying when the NAPI queue is full. The
> + * check is repeated under the queue lock before queueing.
> + */
> + if (tfile->napi_enabled &&
> + unlikely(tun_napi_queue_full(&tfile->sk.sk_write_queue))) {
> + if (msg_control) {
> + struct ubuf_info *uarg = msg_control;
> +
> + uarg->ops->complete(NULL, uarg, false);
> + }
> + dev_core_stats_rx_dropped_inc(tun->dev);
> + return total_len;
> + }
[ ... ]
> @@ -2045,6 +2069,14 @@ static ssize_t tun_get_user(struct tun_struct *tun, struct tun_file *tfile,
> goto free_skb;
> }
>
> + if (unlikely(tun_napi_queue_full(queue))) {
> + spin_unlock_bh(&queue->lock);
> + rcu_read_unlock();
> + kfree_skb_reason(skb, SKB_DROP_REASON_FULL_RING);
> + dev_core_stats_rx_dropped_inc(tun->dev);
> + return total_len;
> + }
> +
> __skb_queue_tail(queue, skb);
> queue_len = skb_queue_len(queue);
> spin_unlock(&queue->lock);
[Severity: Medium]
Can these early returns leave skbs on sk_write_queue with no NAPI
scheduled? In tun_get_user(), NAPI is only scheduled after a successful
enqueue:
if (!more || queue_len > NAPI_POLL_WEIGHT)
napi_schedule(&tfile->napi);
net.core.netdev_max_backlog is handled by proc_dointvec with no lower
bound. An admin can set it below NAPI_POLL_WEIGHT (64), or lower it at
runtime while skbs are already queued.
vhost-net sets MSG_MORE while batching (tx_can_batch()). So a vhost-net
sender can fill the queue to max_backlog + 1 skbs without queue_len
ever going above 64.
The send that ends the batch, without MSG_MORE, then hits either the
early check or this locked check. Both return total_len before the
napi_schedule() call, and every later send is dropped the same way.
The batch path has the same problem. tun_xdp_one() returns 0 on the new
drop, and tun_sendmsg() only schedules when something was queued:
if (tfile->napi_enabled && queued > 0)
napi_schedule(&tfile->napi);
So a batch in which every buffer is dropped never schedules NAPI
either.
Before this patch, the next send without MSG_MORE would queue a packet
and schedule NAPI. With the patch, does RX on that queue stop until the
queue is detached or purged?
With vhost-net zerocopy, the stuck skbs also keep their ubuf_info
references. vhost_net_flush()->vhost_net_ubuf_put_and_wait() does an
uninterruptible wait_event() on those references.
vhost_net_release() calls it before the sockfd_put() of the tx socket,
so the tun detach that would purge the queue cannot happen.
Could releasing or resetting the vhost backend then hang until the tun
device is deleted?
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261031231444.1528758-1-antonio%40mandelbit.com
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-11-01 23:23 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-31 23:14 [PATCH net v3] tun: bound the NAPI receive queue Antonio Quartulli
2026-11-01 2:08 ` Willem de Bruijn
2026-11-01 20:17 ` Antonio Quartulli
2026-11-01 23:23 ` netdev-bot+sashiko
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox