[PATCH net 1/2] net: do not run out of frags in alloc_skb_with_frags()

From: Haishuang Yan

Date: Sat Nov 07 2026 - 21:08:01 EST


alloc_skb_with_frags() rejects data_len above
MAX_SKB_FRAGS * (PAGE_SIZE << order) up front, but then rounds the order
down for the tail, so a tail of N pages is split into one frag per bit
set in N. A data_len that passed the check can then need more than
MAX_SKB_FRAGS frags, and the allocation fails with -ENOBUFS although
every page allocation succeeded.

With order 3 and 4K pages, a 516536 byte packet is 15 * 32K plus a 25016
byte tail, which becomes 16K + 8K + 4K, so 18 frags are needed. Sizes
between about 480K and 540K can be affected.

tun hits this when vhost-net hands it a GSO packet of such a size from a
guest. Nothing on the host limits the size of a guest's GSO packet to
64K: virtio_net_hdr has no total length, and tun_get_user() sizes the
skb from the descriptor chain. Today's virtio-net drivers stay at 64K,
but a guest can send bigger packets by raising its own TSO limit, and
the host must not depend on the guest for that. Worse, vhost-net treats
-ENOBUFS from sendmsg() as transient: handle_tx_copy() puts the
descriptor back and retries it, so the vhost worker spins at 100% on
the same packet and the device stops passing traffic in both
directions. On an unmodified host, tun_get_user() returned -ENOBUFS
about 620,000 times per second, the tap's rx_dropped counter climbed at
the same rate, and this went on with no traffic until the VM was
destroyed.

Keep rounding the tail down when the remaining frags allow it, so no
allocation that works today changes, and use one page rounded up for
the whole tail otherwise. This is never larger than the full-size
chunks already used.

This was triggered while prototyping >64K TSO for virtio-net, with a
one-line guest virtio_net change raising its TSO limit; the host was
unmodified. The fix was tested with that reproducer and with the KUnit
test in the next patch.

Fixes: 09c2c90705bb ("net: allow alloc_skb_with_frags() to allocate bigger packets")
Signed-off-by: Haishuang Yan <yanhaishuang@xxxxxxxxxxxxxxxxxxxx>
---
Base: net 30c71476d842 ("ipv6: Check the Home Address option again before its
address is used"). The reproducer below was run on net-next 303d91a15925,
where alloc_skb_with_frags() is identical. On net: KUnit net_core 20/20
(kunit.py, UML x86_64), and allmodconfig W=1 build of net/core/ (arm64)
with no warnings.

Reproducer: x86_64 KVM host (i7-14700) running net-next 303d91a15925 with no
local changes; vhost-net, a tap NIC with mrg_rxbuf=on and rx_queue_size=1024.
The guest (net-next too) has a local one-line virtio_net change setting
netif_set_tso_max_size(dev, GSO_MAX_SIZE), plus
`ip link set dev ethX gso_max_size 524280 gso_ipv4_max_size 524280`; then
iperf3 guest -> host.

Without this patch: iperf3 fails, one vhost worker stays at 99.9% CPU,
tun_get_user() returns -105 (31014 times in 50 ms, kretprobe), the tap's
rx_dropped goes up by 34M during the run and keeps climbing at ~690k/s with
no traffic, and the guest can no longer ping the host until the VM is
destroyed. With this patch (same setup plus this patch on the host): 81
Gbit/s, no drops, and the vhost worker is idle afterwards.

net/core/skbuff.c | 14 ++++++++++++--
1 file changed, 12 insertions(+), 2 deletions(-)

diff --git a/net/core/skbuff.c b/net/core/skbuff.c
index 8da53d5b76fc..93e7b04ddfe6 100644
--- a/net/core/skbuff.c
+++ b/net/core/skbuff.c
@@ -6902,8 +6902,18 @@ struct sk_buff *alloc_skb_with_frags(unsigned long header_len,
while (data_len) {
if (nr_frags == MAX_SKB_FRAGS)
goto failure;
- while (order && PAGE_ALIGN(data_len) < (PAGE_SIZE << order))
- order--;
+ if (order && PAGE_ALIGN(data_len) < (PAGE_SIZE << order)) {
+ unsigned long pages = PAGE_ALIGN(data_len) >> PAGE_SHIFT;
+
+ /* Rounding the tail down to smaller pages takes one
+ * frag per bit set in its page count. If not enough
+ * frags are left for that, use one bigger page.
+ */
+ if (hweight_long(pages) > MAX_SKB_FRAGS - nr_frags)
+ order = order_base_2(pages);
+ else
+ order = ilog2(pages);
+ }

if (order) {
page = alloc_pages((gfp_mask & ~__GFP_DIRECT_RECLAIM) |
--
2.43.0