summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaolo Abeni <pabeni@redhat.com>2026-10-20 11:52:49 +0200
committerPaolo Abeni <pabeni@redhat.com>2026-10-20 11:52:50 +0200
commit43dcd01eb539160f726b235136295da2e45c19b4 (patch)
treebc4c254a08f18c796dd02853f5c8b3673312c671
parent59089a068f24f53b1f0805aaac147cd700146d5e (diff)
parent2285346afc99d2439f7f031c9d7f30cf045a3815 (diff)
downloadlinux-43dcd01eb539160f726b235136295da2e45c19b4.tar.gz
linux-43dcd01eb539160f726b235136295da2e45c19b4.zip
Merge branch 'net-unify-symmetric-netmem-and-page_pool-refcounting'
Mina Almasry says: ==================== net: unify symmetric netmem and page_pool refcounting netmems (pages and net_iovs) have two independent refcounts: a non-pp refcount (page._refcount or binding->ref) governing backing memory lifetime, and pp_ref_count governing recycling within a page_pool while page_pool holds a single backing non-pp reference. SKBs with pp_recycle=1 own pp_ref_count references on page_pool fragments; SKBs with pp_recycle=0 own non-pp references. Core helpers currently mix these two refcounts asymmetrically: skb_frag_ref() always increments the non-pp refcount via get_netmem(), whereas skb_frag_unref() decrements pp_ref_count when skb->pp_recycle is set. Any path that refs a fragment on a pp_recycle=1 SKB (or unrefs with a hardcoded recycle=false) increments one counter and decrements the other, leaking the backing memory while underflowing pp_ref_count (e.g., IP-TFS [1], skb_split(), skb_shift(), and cloned SKB uncloning). Unclear core APIs also led drivers and ULPs to open-code pp_ref_count manipulations or add one-off helpers like skb_pp_frag_ref(). Unify fragment refcounting into symmetric pairs where each layer clearly specifies which refcount it touches: - Non-PP refcount: get/put_page(), get/put_net_iov(), get/put_netmem() - PP refcount: napi_pp_get/put_page() - SKB netmem: skb_netmem_ref/unref(netmem, recycle) - SKB fragment: skb_frag_ref/unref(skb, f) Found by code inspection while reviewing [1] and confirmed with the KUnit tests in patch 6; not observed in production. Driver and ULP call-site changes are compile-tested only. Link: https://lore.kernel.org/netdev/xfrm-iptfs-pp_ref_count-underflow-v4-1-912fa72106f0@secunet.com/ [1] v2: https://lore.kernel.org/netdev/20261012082148.1698596-1-almasrymina@google.com/ v1: https://lore.kernel.org/netdev/20261010083935.3274178-1-almasrymina@google.com/ ==================== Link: https://patch.msgid.link/20261014173945.373295-1-almasrymina@google.com Signed-off-by: Paolo Abeni <pabeni@redhat.com>
-rw-r--r--drivers/net/ethernet/chelsio/inline_crypto/ch_ktls/chcr_ktls.c2
-rw-r--r--drivers/net/ethernet/marvell/sky2.c2
-rw-r--r--drivers/net/ethernet/mellanox/mlx4/en_rx.c2
-rw-r--r--drivers/net/ethernet/sun/cassini.c4
-rw-r--r--drivers/net/veth.c2
-rw-r--r--include/linux/skbuff_ref.h64
-rw-r--r--include/net/netmem.h24
-rw-r--r--net/core/net_test.c1222
-rw-r--r--net/core/skbuff.c157
-rw-r--r--net/ipv4/esp4.c4
-rw-r--r--net/ipv4/tcp.c6
-rw-r--r--net/ipv6/esp6.c4
-rw-r--r--net/tls/tls_device.c2
-rw-r--r--net/tls/tls_device_fallback.c2
-rw-r--r--net/tls/tls_strp.c2
-rw-r--r--net/xfrm/xfrm_iptfs.c3
16 files changed, 1387 insertions, 115 deletions
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 dbec95b49777..4d645771596a 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
@@ -1660,7 +1660,7 @@ static void chcr_ktls_copy_record_in_skb(struct sk_buff *nskb,
for (i = 0; i < record->num_frags; i++) {
skb_shinfo(nskb)->frags[i] = record->frags[i];
/* increase the frag ref count */
- __skb_frag_ref(&skb_shinfo(nskb)->frags[i]);
+ skb_frag_ref(nskb, i);
}
skb_shinfo(nskb)->nr_frags = record->num_frags;
diff --git a/drivers/net/ethernet/marvell/sky2.c b/drivers/net/ethernet/marvell/sky2.c
index 30facdece3e2..1d42a82db8f2 100644
--- a/drivers/net/ethernet/marvell/sky2.c
+++ b/drivers/net/ethernet/marvell/sky2.c
@@ -2501,7 +2501,7 @@ static void skb_put_frags(struct sk_buff *skb, unsigned int hdr_space,
if (length == 0) {
/* don't need this page */
- __skb_frag_unref(frag, false);
+ skb_frag_unref(skb, i);
--skb_shinfo(skb)->nr_frags;
} else {
size = min(length, (unsigned) PAGE_SIZE);
diff --git a/drivers/net/ethernet/mellanox/mlx4/en_rx.c b/drivers/net/ethernet/mellanox/mlx4/en_rx.c
index 96f97fa0f548..fe18606c2ab5 100644
--- a/drivers/net/ethernet/mellanox/mlx4/en_rx.c
+++ b/drivers/net/ethernet/mellanox/mlx4/en_rx.c
@@ -497,7 +497,7 @@ static int mlx4_en_complete_rx_desc(struct mlx4_en_priv *priv,
fail:
while (nr > 0) {
nr--;
- __skb_frag_unref(skb_shinfo(skb)->frags + nr, false);
+ skb_frag_unref(skb, nr);
}
return 0;
}
diff --git a/drivers/net/ethernet/sun/cassini.c b/drivers/net/ethernet/sun/cassini.c
index 9a8b5cf2d648..02158a1bb2f7 100644
--- a/drivers/net/ethernet/sun/cassini.c
+++ b/drivers/net/ethernet/sun/cassini.c
@@ -1998,7 +1998,7 @@ static int cas_rx_process_pkt(struct cas *cp, struct cas_rx_comp *rxc,
skb->len += hlen - swivel;
skb_frag_fill_page_desc(frag, page->buffer, off, hlen - swivel);
- __skb_frag_ref(frag);
+ skb_frag_ref(skb, 0);
/* any more data? */
if ((words[0] & RX_COMP1_SPLIT_PKT) && ((dlen -= hlen) > 0)) {
@@ -2022,7 +2022,7 @@ static int cas_rx_process_pkt(struct cas *cp, struct cas_rx_comp *rxc,
frag++;
skb_frag_fill_page_desc(frag, page->buffer, 0, hlen);
- __skb_frag_ref(frag);
+ skb_frag_ref(skb, 1);
RX_USED_ADD(page, hlen + cp->crc_size);
}
diff --git a/drivers/net/veth.c b/drivers/net/veth.c
index 38f263d4f06b..c273073b99f5 100644
--- a/drivers/net/veth.c
+++ b/drivers/net/veth.c
@@ -751,7 +751,7 @@ static void veth_xdp_get(struct xdp_buff *xdp)
return;
for (i = 0; i < sinfo->nr_frags; i++)
- __skb_frag_ref(&sinfo->frags[i]);
+ get_netmem(skb_frag_netmem(&sinfo->frags[i]));
}
static int veth_convert_skb_to_xdp_buff(struct veth_rq *rq,
diff --git a/include/linux/skbuff_ref.h b/include/linux/skbuff_ref.h
index 05c8486bafac..f768a3fa1d60 100644
--- a/include/linux/skbuff_ref.h
+++ b/include/linux/skbuff_ref.h
@@ -9,32 +9,53 @@
#include <linux/skbuff.h>
+bool napi_pp_get_page(netmem_ref netmem);
+
/**
- * __skb_frag_ref - take an addition reference on a paged fragment.
- * @frag: the paged fragment
+ * skb_netmem_ref - acquire a fragment reference on a netmem
+ * @netmem: netmem to reference
+ * @recycle: whether page_pool recycling is enabled (e.g. skb->pp_recycle)
*
- * Takes an additional reference on the paged fragment @frag.
+ * Acquires pp_ref_count via napi_pp_get_page() if @recycle is true and
+ * @netmem belongs to a page_pool; otherwise acquires a non-pp backing
+ * reference via get_netmem(). Counterpart to skb_netmem_unref().
*/
-static __always_inline void __skb_frag_ref(skb_frag_t *frag)
+static __always_inline void skb_netmem_ref(netmem_ref netmem, bool recycle)
{
- get_netmem(skb_frag_netmem(frag));
+#ifdef CONFIG_PAGE_POOL
+ if (recycle && napi_pp_get_page(netmem))
+ return;
+#endif
+ get_netmem(netmem);
}
/**
- * skb_frag_ref - take an addition reference on a paged fragment of an skb.
+ * skb_frag_ref - acquire a reference on a paged fragment of an skb
* @skb: the buffer
- * @f: the fragment offset.
+ * @f: the fragment offset
*
- * Takes an additional reference on the @f'th paged fragment of @skb.
+ * Acquires a reference on the @f'th fragment of @skb via skb_netmem_ref()
+ * (pp_ref_count if skb->pp_recycle and page_pool-backed, else non-pp
+ * backing refcount). Counterpart to skb_frag_unref().
*/
static __always_inline void skb_frag_ref(struct sk_buff *skb, int f)
{
- __skb_frag_ref(&skb_shinfo(skb)->frags[f]);
+ skb_netmem_ref(skb_frag_netmem(&skb_shinfo(skb)->frags[f]),
+ skb->pp_recycle);
}
bool napi_pp_put_page(netmem_ref netmem);
-static __always_inline void skb_page_unref(netmem_ref netmem, bool recycle)
+/**
+ * skb_netmem_unref - release a fragment reference on a netmem
+ * @netmem: netmem to unreference
+ * @recycle: whether page_pool recycling is enabled (e.g. skb->pp_recycle)
+ *
+ * Drops pp_ref_count via napi_pp_put_page() if @recycle is true and @netmem
+ * belongs to a page_pool; otherwise drops a non-pp backing reference via
+ * put_netmem(). Counterpart to skb_netmem_ref().
+ */
+static __always_inline void skb_netmem_unref(netmem_ref netmem, bool recycle)
{
#ifdef CONFIG_PAGE_POOL
if (recycle && napi_pp_put_page(netmem))
@@ -44,31 +65,22 @@ static __always_inline void skb_page_unref(netmem_ref netmem, bool recycle)
}
/**
- * __skb_frag_unref - release a reference on a paged fragment.
- * @frag: the paged fragment
- * @recycle: recycle the page if allocated via page_pool
- *
- * Releases a reference on the paged fragment @frag
- * or recycles the page via the page_pool API.
- */
-static __always_inline void __skb_frag_unref(skb_frag_t *frag, bool recycle)
-{
- skb_page_unref(skb_frag_netmem(frag), recycle);
-}
-
-/**
- * skb_frag_unref - release a reference on a paged fragment of an skb.
+ * skb_frag_unref - release a reference on a paged fragment of an skb
* @skb: the buffer
* @f: the fragment offset
*
- * Releases a reference on the @f'th paged fragment of @skb.
+ * Drops a reference on the @f'th fragment of @skb via skb_netmem_unref()
+ * (pp_ref_count if skb->pp_recycle and page_pool-backed, else non-pp
+ * backing refcount), skipping managed zerocopy fragments. Counterpart to
+ * skb_frag_ref().
*/
static __always_inline void skb_frag_unref(struct sk_buff *skb, int f)
{
struct skb_shared_info *shinfo = skb_shinfo(skb);
if (!skb_zcopy_managed(skb))
- __skb_frag_unref(&shinfo->frags[f], skb->pp_recycle);
+ skb_netmem_unref(skb_frag_netmem(&shinfo->frags[f]),
+ skb->pp_recycle);
}
#endif /* _LINUX_SKBUFF_REF_H */
diff --git a/include/net/netmem.h b/include/net/netmem.h
index c4beb6fbc130..193de0d21490 100644
--- a/include/net/netmem.h
+++ b/include/net/netmem.h
@@ -409,21 +409,37 @@ static inline bool net_is_devmem_iov(const struct net_iov *niov)
}
#endif
-void __get_netmem(netmem_ref netmem);
-void __put_netmem(netmem_ref netmem);
+void get_net_iov(struct net_iov *niov);
+void put_net_iov(struct net_iov *niov);
+/**
+ * get_netmem - acquire a non-page_pool reference on a netmem
+ * @netmem: netmem to reference
+ *
+ * Acquires a non-pp backing reference (page._refcount via get_page() or
+ * binding->ref via get_net_iov()) regardless of page_pool membership.
+ * Counterpart to put_netmem().
+ */
static __always_inline void get_netmem(netmem_ref netmem)
{
if (netmem_is_net_iov(netmem))
- __get_netmem(netmem);
+ get_net_iov(netmem_to_net_iov(netmem));
else
get_page(netmem_to_page(netmem));
}
+/**
+ * put_netmem - release a non-page_pool reference on a netmem
+ * @netmem: netmem to unreference
+ *
+ * Drops a non-pp backing reference (page._refcount via put_page() or
+ * binding->ref via put_net_iov()) regardless of page_pool membership.
+ * Counterpart to get_netmem().
+ */
static __always_inline void put_netmem(netmem_ref netmem)
{
if (netmem_is_net_iov(netmem))
- __put_netmem(netmem);
+ put_net_iov(netmem_to_net_iov(netmem));
else
put_page(netmem_to_page(netmem));
}
diff --git a/net/core/net_test.c b/net/core/net_test.c
index bc2ef7612e7d..2bb01430a89f 100644
--- a/net/core/net_test.c
+++ b/net/core/net_test.c
@@ -457,10 +457,1231 @@ static void ip_tunnel_flags_test_run(struct kunit *test)
KUNIT_ASSERT_TRUE(test, __ipt_flag_op(bitmap_equal, exp, out));
}
+/* Netmem & SKB fragment refcounting */
+
+#include <linux/skbuff_ref.h>
+#include <net/netmem.h>
+#include <net/netdev_rx_queue.h>
+#include <net/page_pool/helpers.h>
+#include <net/page_pool/memory_provider.h>
+#include <net/tcp.h>
+#include "devmem.h"
+#include "netmem_priv.h"
+
+static void netmem_ref_test_page(struct kunit *test)
+{
+ struct sk_buff *skb;
+ netmem_ref netmem;
+ struct page *page;
+ int base_ref;
+
+ page = alloc_page(GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, page);
+ netmem = page_to_netmem(page);
+ base_ref = page_ref_count(page);
+
+ /* Layer 1: get_netmem / put_netmem use page non-pp refcount */
+ get_netmem(netmem);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref + 1);
+ put_netmem(netmem);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref);
+
+#ifdef CONFIG_PAGE_POOL
+ /* Layer 2: napi_pp_get/put_page return false on non-PP page */
+ KUNIT_EXPECT_FALSE(test, napi_pp_get_page(netmem));
+ KUNIT_EXPECT_FALSE(test, napi_pp_put_page(netmem));
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref);
+#endif
+
+ /* Layer 3: skb_netmem_ref / unref fall back to non-pp on non-PP page */
+ skb_netmem_ref(netmem, false);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref + 1);
+ skb_netmem_unref(netmem, false);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref);
+
+ skb_netmem_ref(netmem, true);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref + 1);
+ skb_netmem_unref(netmem, true);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref);
+
+ /* Layer 4: skb_frag_ref / skb_frag_unref on non-PP page */
+ skb = alloc_skb(64, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, skb);
+ skb_fill_netmem_desc(skb, 0, netmem, 0, 64);
+
+ skb->pp_recycle = 1;
+ skb_frag_ref(skb, 0);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref + 1);
+ skb_frag_unref(skb, 0);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref);
+
+ skb_shinfo(skb)->nr_frags = 0;
+ consume_skb(skb);
+ __free_page(page);
+}
+
+#ifdef CONFIG_PAGE_POOL
+static void netmem_ref_test_pp_page(struct kunit *test)
+{
+ struct page_pool_params pp_params = {
+ .order = 0,
+ .pool_size = 4,
+ .nid = NUMA_NO_NODE,
+ };
+ struct page_pool *pool;
+ struct sk_buff *skb, *copy, *clone, *split;
+ netmem_ref netmem;
+ struct page *page;
+ long base_pp_ref;
+ int base_ref;
+
+ pool = page_pool_create(&pp_params);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, pool);
+
+ page = page_pool_dev_alloc_pages(pool);
+ KUNIT_ASSERT_NOT_NULL(test, page);
+ netmem = page_to_netmem(page);
+ base_ref = page_ref_count(page);
+ base_pp_ref = atomic_long_read(&page->pp_ref_count);
+
+ /* Layer 1: get_netmem / put_netmem always use non-pp refcount */
+ get_netmem(netmem);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref + 1);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref);
+ put_netmem(netmem);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref);
+
+ /* Layer 2: napi_pp_get/put_page use pp_ref_count */
+ KUNIT_EXPECT_TRUE(test, napi_pp_get_page(netmem));
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref + 1);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref);
+ KUNIT_EXPECT_TRUE(test, napi_pp_put_page(netmem));
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref);
+
+ /* Layer 3: skb_netmem_ref / unref obey recycle flag */
+ skb_netmem_ref(netmem, false);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref + 1);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref);
+ skb_netmem_unref(netmem, false);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref);
+
+ skb_netmem_ref(netmem, true);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref + 1);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref);
+ skb_netmem_unref(netmem, true);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref);
+
+ /* Layer 4: skb_frag_ref / skb_frag_unref match on pp_recycle=1 & 0 */
+ skb = alloc_skb(64, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, skb);
+ skb_fill_netmem_desc(skb, 0, netmem, 0, 64);
+ skb->len = 64;
+ skb->data_len = 64;
+
+ skb->pp_recycle = 1;
+ skb_frag_ref(skb, 0);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref + 1);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref);
+ skb_frag_unref(skb, 0);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref);
+
+ /* pskb_copy on pp_recycle=1 skb propagates pp_recycle & pp_ref_count */
+ copy = pskb_copy(skb, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, copy);
+ KUNIT_EXPECT_TRUE(test, copy->pp_recycle);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref + 1);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref);
+ consume_skb(copy);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref);
+
+ /* skb_zerocopy on pp_recycle=1 skb propagates pp_ref_count */
+ copy = alloc_skb(0, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, copy);
+ skb_put(copy, skb_tailroom(copy));
+ skb->head_frag = 1;
+ KUNIT_ASSERT_EQ(test, skb_zerocopy(copy, skb, skb->len, 0), 0);
+ skb->head_frag = 0;
+ KUNIT_EXPECT_TRUE(test, copy->pp_recycle);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref + 1);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref);
+ consume_skb(copy);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref);
+
+ /* skb_clone + pskb_expand_head preserves pp_recycle and pp_ref_count */
+ clone = skb_clone(skb, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, clone);
+ KUNIT_ASSERT_EQ(test, pskb_expand_head(clone, 32, 0, GFP_KERNEL), 0);
+ KUNIT_EXPECT_TRUE(test, clone->pp_recycle);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref + 1);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref);
+ consume_skb(clone);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref);
+
+ /* skb_split inside linear header & inside nonlinear page_pool frags */
+ skb_shinfo(skb)->nr_frags = 0;
+ skb->len = 0;
+ skb->data_len = 0;
+ skb_reset_tail_pointer(skb);
+ skb_put(skb, 32);
+ skb_fill_netmem_desc(skb, 0, netmem, 0, 64);
+ skb->len += 64;
+ skb->data_len = 64;
+ split = alloc_skb(32, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, split);
+ skb_split(skb, split, 16);
+ KUNIT_EXPECT_TRUE(test, split->pp_recycle);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref);
+ skb_shinfo(split)->nr_frags = 0;
+ consume_skb(split);
+ skb->len = 0;
+ skb->data_len = 0;
+ skb_reset_tail_pointer(skb);
+ skb_fill_netmem_desc(skb, 0, netmem, 0, 64);
+ skb->len = 64;
+ skb->data_len = 64;
+
+ split = alloc_skb(0, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, split);
+ skb_split(skb, split, 32);
+ KUNIT_EXPECT_TRUE(test, split->pp_recycle);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref + 1);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref);
+ consume_skb(split);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref);
+
+ /* skb_segment on pp_recycle=1 skb propagates pp_recycle to segments */
+ skb->protocol = htons(ETH_P_IP);
+ skb_reset_mac_header(skb);
+ skb_reset_network_header(skb);
+ skb_shinfo(skb)->gso_size = 16;
+ split = skb_segment(skb, NETIF_F_SG | NETIF_F_HW_CSUM);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(split));
+ KUNIT_EXPECT_TRUE(test, split->pp_recycle);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref + 2);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref);
+ kfree_skb_list(split);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref);
+
+ /* skb_shift balances pp_ref_count across split, partial coalesce,
+ * multi-frag coalesce + partial next-frag shift, and full merge.
+ */
+ skb_fill_netmem_desc(skb, 0, netmem, 0, 32);
+ skb_fill_netmem_desc(skb, 1, netmem, 64, 32);
+ page_pool_ref_page(page);
+ skb->len = 64;
+ skb->data_len = 64;
+ split = alloc_skb(0, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, split);
+ split->pp_recycle = 1;
+ KUNIT_EXPECT_EQ(test, skb_shift(split, skb, 16), 16);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref + 2);
+ KUNIT_EXPECT_EQ(test, skb_shift(split, skb, 8), 8);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref + 2);
+ KUNIT_EXPECT_EQ(test, skb_shift(split, skb, 24), 24);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref + 2);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref);
+ KUNIT_EXPECT_EQ(test, skb_shift(split, skb, 16), 16);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref + 1);
+ skb_shinfo(split)->nr_frags = 0;
+ consume_skb(split);
+ page_pool_put_full_page(pool, page, false);
+ skb_fill_netmem_desc(skb, 0, netmem, 0, 64);
+ skb->len = 64;
+ skb->data_len = 64;
+
+ skb->pp_recycle = 0;
+ skb_frag_ref(skb, 0);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref + 1);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref);
+ skb_frag_unref(skb, 0);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref);
+
+ skb_shinfo(skb)->nr_frags = 0;
+ consume_skb(skb);
+ page_pool_put_full_page(pool, page, false);
+ page_pool_destroy(pool);
+
+ /* Order-1 compound page_pool page: tail netmem resolves to head &
+ * build_skb(head_frag=1, pp_recycle=1) skb_zerocopy uses pp_ref_count.
+ */
+ pp_params.order = 1;
+ pool = page_pool_create(&pp_params);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, pool);
+ page = page_pool_dev_alloc_pages(pool);
+ KUNIT_ASSERT_NOT_NULL(test, page);
+ base_ref = page_ref_count(page);
+ base_pp_ref = atomic_long_read(&page->pp_ref_count);
+
+ netmem = page_to_netmem(page + 1);
+ KUNIT_EXPECT_TRUE(test, napi_pp_get_page(netmem));
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref + 1);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref);
+ KUNIT_EXPECT_TRUE(test, napi_pp_put_page(netmem));
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref);
+
+ skb = build_skb(page_address(page), PAGE_SIZE);
+ KUNIT_ASSERT_NOT_NULL(test, skb);
+ skb->pp_recycle = 1;
+ skb_put(skb, 64);
+ page_pool_ref_page(page);
+
+ copy = alloc_skb(0, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, copy);
+ skb_put(copy, skb_tailroom(copy));
+ KUNIT_ASSERT_EQ(test, skb_zerocopy(copy, skb, skb->len, 0), 0);
+ KUNIT_EXPECT_TRUE(test, copy->pp_recycle);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref + 2);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref);
+ consume_skb(copy);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref + 1);
+ consume_skb(skb);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref);
+
+ page_pool_put_full_page(pool, page, false);
+ page_pool_destroy(pool);
+}
+#endif
+
+#if defined(CONFIG_PAGE_POOL) && defined(CONFIG_NET_DEVMEM)
+static void dummy_percpu_ref_release(struct percpu_ref *ref)
+{
+}
+
+static void netmem_ref_test_net_iov(struct kunit *test)
+{
+ struct net_devmem_dmabuf_binding binding = {};
+ struct page_pool_params pp_params = {
+ .order = 0,
+ .pool_size = 4,
+ .nid = NUMA_NO_NODE,
+ };
+ struct sk_buff *skb, *clone, *copy;
+ struct page_pool *pool;
+ struct net_iov niov = {};
+ netmem_ref netmem;
+ long base_ref;
+ int err;
+
+ err = percpu_ref_init(&binding.ref, dummy_percpu_ref_release,
+ PERCPU_REF_INIT_ATOMIC, GFP_KERNEL);
+ KUNIT_ASSERT_EQ(test, err, 0);
+
+ net_iov_init(&niov, &binding.area, NET_IOV_DMABUF);
+ netmem = net_iov_to_netmem(&niov);
+ base_ref = atomic_long_read(&binding.ref.data->count);
+
+ /* Non-PP net_iov: get/put_net_iov & get/put_netmem */
+ get_net_iov(&niov);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&binding.ref.data->count),
+ base_ref + 1);
+ put_net_iov(&niov);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&binding.ref.data->count),
+ base_ref);
+
+ KUNIT_EXPECT_FALSE(test, napi_pp_get_page(netmem));
+ KUNIT_EXPECT_FALSE(test, napi_pp_put_page(netmem));
+
+ skb_netmem_ref(netmem, true);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&binding.ref.data->count),
+ base_ref + 1);
+ skb_netmem_unref(netmem, true);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&binding.ref.data->count),
+ base_ref);
+
+ /* Attach net_iov to a page_pool to test PP net_iov behavior */
+ pool = page_pool_create(&pp_params);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, pool);
+ netmem_set_pp(netmem, pool);
+ netmem_or_pp_magic(netmem, PP_SIGNATURE);
+ atomic_long_set(&niov.desc.pp_ref_count, 2);
+
+ /* Layer 0 & 1: get_net_iov / get_netmem grab non-pp ref even on PP */
+ get_net_iov(&niov);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&binding.ref.data->count),
+ base_ref + 1);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&niov.desc.pp_ref_count), 2L);
+ put_net_iov(&niov);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&binding.ref.data->count),
+ base_ref);
+
+ /* Layer 2: napi_pp_get/put_page use pp_ref_count */
+ KUNIT_EXPECT_TRUE(test, napi_pp_get_page(netmem));
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&niov.desc.pp_ref_count), 3L);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&binding.ref.data->count),
+ base_ref);
+ KUNIT_EXPECT_TRUE(test, napi_pp_put_page(netmem));
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&niov.desc.pp_ref_count), 2L);
+
+ /* Layer 4: skb_frag_ref / skb_frag_unref on PP net_iov */
+ skb = alloc_skb(64, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, skb);
+ skb_fill_netmem_desc(skb, 0, netmem, 0, 64);
+ skb->len = 64;
+ skb->data_len = 64;
+
+ skb->pp_recycle = 1;
+ skb_frag_ref(skb, 0);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&niov.desc.pp_ref_count), 3L);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&binding.ref.data->count),
+ base_ref);
+ skb_frag_unref(skb, 0);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&niov.desc.pp_ref_count), 2L);
+
+ /* pskb_copy on pp_recycle=1 net_iov skb keeps pp_ref_count */
+ copy = pskb_copy(skb, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, copy);
+ KUNIT_EXPECT_TRUE(test, copy->pp_recycle);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&niov.desc.pp_ref_count), 3L);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&binding.ref.data->count),
+ base_ref);
+ consume_skb(copy);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&niov.desc.pp_ref_count), 2L);
+
+ /* skb_zerocopy on pp_recycle=1 net_iov skb keeps pp_ref_count */
+ copy = alloc_skb(0, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, copy);
+ skb_put(copy, skb_tailroom(copy));
+ skb->head_frag = 1;
+ skb->unreadable = 1;
+ KUNIT_ASSERT_EQ(test, skb_zerocopy(copy, skb, skb->len, 0), 0);
+ skb->head_frag = 0;
+ KUNIT_EXPECT_TRUE(test, copy->pp_recycle);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&niov.desc.pp_ref_count), 3L);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&binding.ref.data->count),
+ base_ref);
+ consume_skb(copy);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&niov.desc.pp_ref_count), 2L);
+
+ /* skb_shift balances pp_ref_count on split and merge */
+ copy = alloc_skb(0, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, copy);
+ copy->pp_recycle = 1;
+ KUNIT_EXPECT_EQ(test, skb_shift(copy, skb, 16), 16);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&niov.desc.pp_ref_count), 3L);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&binding.ref.data->count),
+ base_ref);
+ KUNIT_EXPECT_EQ(test, skb_shift(copy, skb, 48), 48);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&niov.desc.pp_ref_count), 2L);
+ skb_shinfo(copy)->nr_frags = 0;
+ consume_skb(copy);
+ skb_fill_netmem_desc(skb, 0, netmem, 0, 64);
+ skb->len = 64;
+ skb->data_len = 64;
+
+ /* Uncloning a cloned pp_recycle=1 net_iov skb keeps pp_ref_count */
+ clone = skb_clone(skb, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, clone);
+ KUNIT_ASSERT_EQ(test, pskb_expand_head(skb, 32, 0, GFP_KERNEL), 0);
+ KUNIT_EXPECT_TRUE(test, skb->pp_recycle);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&niov.desc.pp_ref_count), 3L);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&binding.ref.data->count),
+ base_ref);
+ consume_skb(clone);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&niov.desc.pp_ref_count), 2L);
+
+ skb->pp_recycle = 0;
+ skb_frag_ref(skb, 0);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&binding.ref.data->count),
+ base_ref + 1);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&niov.desc.pp_ref_count), 2L);
+ skb_frag_unref(skb, 0);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&binding.ref.data->count),
+ base_ref);
+
+ skb_shinfo(skb)->nr_frags = 0;
+ consume_skb(skb);
+ netmem_clear_pp_magic(netmem);
+ netmem_set_pp(netmem, NULL);
+ page_pool_destroy(pool);
+ percpu_ref_exit(&binding.ref);
+}
+
+static void netmem_ref_test_release_data(struct kunit *test)
+{
+ struct page_pool_params pp_params = {
+ .order = 0,
+ .pool_size = 4,
+ .nid = NUMA_NO_NODE,
+ };
+ struct sk_buff *skb, *clone1, *clone2, *ext;
+ struct net_devmem_dmabuf_binding binding = {};
+ struct net_iov niov = {};
+ struct page_pool *pool;
+ struct page *page;
+ netmem_ref netmem;
+ long base_pp_ref;
+ int base_ref;
+ int i;
+
+ pool = page_pool_create(&pp_params);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(pool));
+ page = page_pool_dev_alloc_pages(pool);
+ KUNIT_ASSERT_NOT_NULL(test, page);
+ netmem = page_to_netmem(page);
+ page_pool_ref_netmem(netmem);
+
+ /* 3-way clone chain: only last clone drops pp_ref_count */
+ skb = alloc_skb(64, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, skb);
+ skb_put(skb, 32);
+ skb->pp_recycle = 1;
+ skb_fill_netmem_desc(skb, 0, netmem, 0, 64);
+ skb->len += 64;
+ skb->data_len = 64;
+ base_ref = page_ref_count(page);
+ base_pp_ref = atomic_long_read(&page->pp_ref_count);
+
+ page_pool_ref_netmem(netmem);
+ clone1 = skb_clone(skb, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, clone1);
+ clone2 = skb_clone(clone1, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, clone2);
+
+ /* Freeing head skb first leaves clone1/clone2 pp_recycle=1 intact */
+ consume_skb(skb);
+ KUNIT_EXPECT_TRUE(test, clone1->pp_recycle);
+ KUNIT_EXPECT_TRUE(test, clone2->pp_recycle);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref + 1);
+ consume_skb(clone2);
+ KUNIT_EXPECT_TRUE(test, clone1->pp_recycle);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref + 1);
+ consume_skb(clone1);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref);
+
+ /* All 4 combinations of (unclone skb vs clone) x (free order) */
+ for (i = 0; i < 4; i++) {
+ skb = alloc_skb(64, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, skb);
+ skb_put(skb, 32);
+ skb->pp_recycle = 1;
+ skb_fill_netmem_desc(skb, 0, netmem, 0, 64);
+ skb->len += 64;
+ skb->data_len = 64;
+ page_pool_ref_netmem(netmem);
+
+ clone1 = skb_clone(skb, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, clone1);
+ KUNIT_ASSERT_EQ(test,
+ pskb_expand_head((i & 1) ? clone1 : skb,
+ 32, 0, GFP_KERNEL), 0);
+ KUNIT_EXPECT_TRUE(test, skb->pp_recycle);
+ KUNIT_EXPECT_TRUE(test, clone1->pp_recycle);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref + 2);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref);
+
+ if (i & 2) {
+ consume_skb(clone1);
+ KUNIT_EXPECT_EQ(test,
+ atomic_long_read(&page->pp_ref_count),
+ base_pp_ref + 1);
+ consume_skb(skb);
+ } else {
+ consume_skb(skb);
+ KUNIT_EXPECT_EQ(test,
+ atomic_long_read(&page->pp_ref_count),
+ base_pp_ref + 1);
+ consume_skb(clone1);
+ }
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref);
+ }
+
+ /* pskb_extract carving inside header & inside nonlinear frags */
+ skb = alloc_skb(64, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, skb);
+ skb_put(skb, 32);
+ skb->pp_recycle = 1;
+ skb_fill_netmem_desc(skb, 0, netmem, 0, 64);
+ skb->len += 64;
+ skb->data_len = 64;
+ page_pool_ref_netmem(netmem);
+
+ /* Carve inside header keeping frag -> pp_ref_count +1 until free */
+ ext = pskb_extract(skb, 16, skb->len - 16, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, ext);
+ KUNIT_EXPECT_TRUE(test, ext->pp_recycle);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref + 2);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref);
+ consume_skb(ext);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref + 1);
+
+ /* Carve + trim + skb_condense ref/unrefs pp_ref_count cleanly */
+ ext = pskb_extract(skb, 16, 48, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, ext);
+ KUNIT_EXPECT_TRUE(test, ext->pp_recycle);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref + 1);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref);
+ consume_skb(ext);
+
+ /* Carve inside nonlinear keeping frag -> pp_ref_count +1 until free */
+ ext = pskb_extract(skb, 48, skb->len - 48, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, ext);
+ KUNIT_EXPECT_TRUE(test, ext->pp_recycle);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref + 2);
+ KUNIT_EXPECT_EQ(test, page_ref_count(page), base_ref);
+ consume_skb(ext);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref + 1);
+
+ /* skb_morph drops old skb data via skb_release_data */
+ page_pool_ref_netmem(netmem);
+ clone1 = alloc_skb(64, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, clone1);
+ clone1->pp_recycle = 1;
+ skb_fill_netmem_desc(clone1, 0, netmem, 0, 64);
+ clone1->len = 64;
+ clone1->data_len = 64;
+ skb_morph(clone1, skb);
+ KUNIT_EXPECT_TRUE(test, clone1->pp_recycle);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&page->pp_ref_count),
+ base_pp_ref + 1);
+ consume_skb(clone1);
+ kfree_skb(skb);
+ page_pool_put_full_page(pool, page, false);
+ page_pool_put_full_page(pool, page, false);
+
+ /* Also verify pskb_extract on page_pool net_iov */
+ KUNIT_ASSERT_EQ(test,
+ percpu_ref_init(&binding.ref, dummy_percpu_ref_release,
+ PERCPU_REF_INIT_ATOMIC, GFP_KERNEL),
+ 0);
+ net_iov_init(&niov, &binding.area, NET_IOV_DMABUF);
+ netmem = net_iov_to_netmem(&niov);
+ netmem_set_pp(netmem, pool);
+ netmem_or_pp_magic(netmem, PP_SIGNATURE);
+ atomic_long_set(&niov.desc.pp_ref_count, 2);
+
+ skb = alloc_skb(64, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, skb);
+ skb_put(skb, 32);
+ skb->pp_recycle = 1;
+ skb_fill_netmem_desc(skb, 0, netmem, 0, 64);
+ skb->len += 64;
+ skb->data_len = 64;
+
+ ext = pskb_extract(skb, 48, skb->len - 48, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, ext);
+ KUNIT_EXPECT_TRUE(test, ext->pp_recycle);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&niov.desc.pp_ref_count), 3L);
+ consume_skb(ext);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&niov.desc.pp_ref_count), 2L);
+
+ skb_shinfo(skb)->nr_frags = 0;
+ consume_skb(skb);
+ netmem_clear_pp_magic(netmem);
+ netmem_set_pp(netmem, NULL);
+ page_pool_destroy(pool);
+ percpu_ref_exit(&binding.ref);
+}
+
+static void netmem_ref_test_skb_segment(struct kunit *test)
+{
+ struct page_pool_params pp_params = {
+ .order = 0,
+ .pool_size = 4,
+ .nid = NUMA_NO_NODE,
+ };
+ struct ubuf_info uarg = {
+ .flags = SKBFL_ZEROCOPY_ENABLE,
+ };
+ struct sk_buff *head, *list, *segs, *seg;
+ struct net_devmem_dmabuf_binding binding = {};
+ struct net_iov niov = {};
+ struct page *pp_page, *reg_page;
+ netmem_ref pp_nm, niov_nm;
+ struct page_pool *pool;
+ long base_pp_ref, base_iov_ref;
+ int base_pp_page_ref, base_reg_ref;
+
+ pool = page_pool_create(&pp_params);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(pool));
+ pp_page = page_pool_dev_alloc_pages(pool);
+ KUNIT_ASSERT_NOT_NULL(test, pp_page);
+ pp_nm = page_to_netmem(pp_page);
+ page_pool_ref_netmem(pp_nm);
+ reg_page = alloc_page(GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, reg_page);
+
+ KUNIT_ASSERT_EQ(test,
+ percpu_ref_init(&binding.ref, dummy_percpu_ref_release,
+ PERCPU_REF_INIT_ATOMIC, GFP_KERNEL),
+ 0);
+ net_iov_init(&niov, &binding.area, NET_IOV_DMABUF);
+ niov_nm = net_iov_to_netmem(&niov);
+ netmem_set_pp(niov_nm, pool);
+ netmem_or_pp_magic(niov_nm, PP_SIGNATURE);
+ atomic_long_set(&niov.desc.pp_ref_count, 2);
+
+ base_pp_ref = atomic_long_read(&pp_page->pp_ref_count);
+ base_pp_page_ref = page_ref_count(pp_page);
+ base_reg_ref = page_ref_count(reg_page);
+ base_iov_ref = atomic_long_read(&binding.ref.data->count);
+
+ /* 1. Single GSO skb with page_pool net_iov frags */
+ head = alloc_skb(64, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, head);
+ head->pp_recycle = 1;
+ head->protocol = htons(ETH_P_IP);
+ skb_reset_mac_header(head);
+ skb_reset_network_header(head);
+ skb_fill_netmem_desc(head, 0, niov_nm, 0, 64);
+ head->len = 64;
+ head->data_len = 64;
+ skb_shinfo(head)->gso_size = 16;
+
+ segs = skb_segment(head, NETIF_F_SG | NETIF_F_HW_CSUM);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(segs));
+ for (seg = segs; seg; seg = seg->next)
+ KUNIT_EXPECT_TRUE(test, seg->pp_recycle);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&niov.desc.pp_ref_count), 6L);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&binding.ref.data->count),
+ base_iov_ref);
+ kfree_skb_list(segs);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&niov.desc.pp_ref_count), 2L);
+ skb_shinfo(head)->nr_frags = 0;
+ consume_skb(head);
+
+ /* 2. GRO frag_list fast path (skb_clone of pp_recycle=1 list_skb) */
+ head = alloc_skb(64, GFP_KERNEL);
+ list = alloc_skb(64, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, head);
+ KUNIT_ASSERT_NOT_NULL(test, list);
+ head->pp_recycle = 1;
+ list->pp_recycle = 1;
+ head->protocol = htons(ETH_P_IP);
+ skb_reset_mac_header(head);
+ skb_reset_network_header(head);
+ skb_put(head, 16);
+ skb_fill_netmem_desc(head, 0, pp_nm, 0, 16);
+ head->len += 16;
+ head->data_len = 16;
+ page_pool_ref_netmem(pp_nm);
+
+ skb_put(list, 16);
+ skb_fill_netmem_desc(list, 0, pp_nm, 16, 16);
+ list->len += 16;
+ list->data_len = 16;
+ page_pool_ref_netmem(pp_nm);
+
+ skb_shinfo(head)->frag_list = list;
+ head->len += list->len;
+ head->data_len += list->len;
+ skb_shinfo(head)->gso_size = 32;
+
+ segs = skb_segment(head, NETIF_F_SG | NETIF_F_HW_CSUM);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(segs));
+ for (seg = segs; seg; seg = seg->next)
+ KUNIT_EXPECT_TRUE(test, seg->pp_recycle);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&pp_page->pp_ref_count),
+ base_pp_ref + 4);
+ KUNIT_EXPECT_EQ(test, page_ref_count(pp_page), base_pp_page_ref);
+ kfree_skb_list(segs);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&pp_page->pp_ref_count),
+ base_pp_ref + 2);
+ consume_skb(head);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&pp_page->pp_ref_count),
+ base_pp_ref);
+
+ /* 3. Mixed GRO frag_list (head pp_recycle=1 + list pp_recycle=0):
+ * straddling segment linearizes its pp_recycle=1 prefix before
+ * attaching the pp_recycle=0 frag so no nskb mixes incompatible
+ * fragment refcount types; non-straddling segments stay zero-copy SG.
+ */
+ head = alloc_skb(64, GFP_KERNEL);
+ list = alloc_skb(0, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, head);
+ KUNIT_ASSERT_NOT_NULL(test, list);
+ head->pp_recycle = 1;
+ list->pp_recycle = 0;
+ head->protocol = htons(ETH_P_IP);
+ skb_reset_mac_header(head);
+ skb_reset_network_header(head);
+ skb_fill_netmem_desc(head, 0, pp_nm, 0, 24);
+ head->len = 24;
+ head->data_len = 24;
+ page_pool_ref_netmem(pp_nm);
+
+ skb_fill_page_desc(list, 0, reg_page, 0, 24);
+ get_page(reg_page);
+ list->len = 24;
+ list->data_len = 24;
+
+ skb_shinfo(head)->frag_list = list;
+ head->len += list->len;
+ head->data_len += list->len;
+ skb_shinfo(head)->gso_size = 16;
+
+ segs = skb_segment(head, NETIF_F_SG | NETIF_F_HW_CSUM);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(segs));
+ KUNIT_EXPECT_TRUE(test, segs->pp_recycle);
+ KUNIT_EXPECT_FALSE(test, segs->next->pp_recycle);
+ KUNIT_EXPECT_FALSE(test, segs->next->next->pp_recycle);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&pp_page->pp_ref_count),
+ base_pp_ref + 2);
+ KUNIT_EXPECT_EQ(test, page_ref_count(pp_page), base_pp_page_ref);
+ KUNIT_EXPECT_EQ(test, page_ref_count(reg_page), base_reg_ref + 3);
+ kfree_skb_list(segs);
+ consume_skb(head);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&pp_page->pp_ref_count),
+ base_pp_ref);
+ KUNIT_EXPECT_EQ(test, page_ref_count(pp_page), base_pp_page_ref);
+ KUNIT_EXPECT_EQ(test, page_ref_count(reg_page), base_reg_ref);
+
+ /* 4. Reverse mixed frag_list (head pp_recycle=0 + list pp_recycle=1):
+ * Crossing from pp_recycle=0 head into pp_recycle=1 list_skb must not
+ * elevate page_ref_count(pp_page) instead of pp_ref_count.
+ */
+ head = alloc_skb(64, GFP_KERNEL);
+ list = alloc_skb(0, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, head);
+ KUNIT_ASSERT_NOT_NULL(test, list);
+ head->pp_recycle = 0;
+ list->pp_recycle = 1;
+ head->protocol = htons(ETH_P_IP);
+ skb_reset_mac_header(head);
+ skb_reset_network_header(head);
+ skb_fill_page_desc(head, 0, reg_page, 0, 24);
+ get_page(reg_page);
+ head->len = 24;
+ head->data_len = 24;
+
+ skb_fill_netmem_desc(list, 0, pp_nm, 0, 24);
+ page_pool_ref_netmem(pp_nm);
+ list->len = 24;
+ list->data_len = 24;
+
+ skb_shinfo(head)->frag_list = list;
+ head->len += list->len;
+ head->data_len += list->len;
+ skb_shinfo(head)->gso_size = 16;
+
+ segs = skb_segment(head, NETIF_F_SG | NETIF_F_HW_CSUM);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(segs));
+ KUNIT_EXPECT_FALSE(test, segs->pp_recycle);
+ KUNIT_EXPECT_TRUE(test, segs->next->pp_recycle);
+ KUNIT_EXPECT_TRUE(test, segs->next->next->pp_recycle);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&pp_page->pp_ref_count),
+ base_pp_ref + 3);
+ KUNIT_EXPECT_EQ(test, page_ref_count(pp_page), base_pp_page_ref);
+ KUNIT_EXPECT_EQ(test, page_ref_count(reg_page), base_reg_ref + 2);
+ kfree_skb_list(segs);
+ consume_skb(head);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&pp_page->pp_ref_count),
+ base_pp_ref);
+ KUNIT_EXPECT_EQ(test, page_ref_count(pp_page), base_pp_page_ref);
+ KUNIT_EXPECT_EQ(test, page_ref_count(reg_page), base_reg_ref);
+
+ /* 5. Linear head_skb (nr_frags=0) + mixed pp_recycle frag_list where
+ * member A has linear + frag (pp_recycle=1) and tail member B has
+ * linear len == skb_headlen(A) (pp_recycle=0): keeps SG clone path
+ * synchronized without hitting BUG_ON(i >= nfrags).
+ */
+ head = alloc_skb(64, GFP_KERNEL);
+ list = alloc_skb(64, GFP_KERNEL);
+ seg = alloc_skb(64, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, head);
+ KUNIT_ASSERT_NOT_NULL(test, list);
+ KUNIT_ASSERT_NOT_NULL(test, seg);
+ head->pp_recycle = 0;
+ list->pp_recycle = 1;
+ seg->pp_recycle = 0;
+ head->protocol = htons(ETH_P_IP);
+ skb_reset_mac_header(head);
+ skb_reset_network_header(head);
+ skb_put(head, 32);
+ skb_put(list, 8);
+ skb_fill_netmem_desc(list, 0, pp_nm, 0, 24);
+ page_pool_ref_netmem(pp_nm);
+ list->len += 24;
+ list->data_len = 24;
+ skb_put(seg, 8);
+ list->next = seg;
+ skb_shinfo(head)->frag_list = list;
+ head->len += list->len + seg->len;
+ head->data_len = list->len + seg->len;
+ skb_shinfo(head)->gso_size = 32;
+
+ segs = skb_segment(head, NETIF_F_SG | NETIF_F_HW_CSUM);
+ KUNIT_ASSERT_FALSE(test, IS_ERR_OR_NULL(segs));
+ KUNIT_EXPECT_FALSE(test, segs->pp_recycle);
+ KUNIT_EXPECT_TRUE(test, segs->next->pp_recycle);
+ KUNIT_EXPECT_FALSE(test, segs->next->next->pp_recycle);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&pp_page->pp_ref_count),
+ base_pp_ref + 2);
+ KUNIT_EXPECT_EQ(test, page_ref_count(pp_page), base_pp_page_ref);
+ kfree_skb_list(segs);
+ consume_skb(head);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&pp_page->pp_ref_count),
+ base_pp_ref);
+
+ /* 6. skb_zerocopy -EFAULT rollback when from->head_frag (pp_page) is
+ * followed by unreadable net_iov frag drops pp_ref_count via
+ * skb_frag_unref(to, 0).
+ */
+ head = build_skb(page_address(pp_page), PAGE_SIZE);
+ KUNIT_ASSERT_NOT_NULL(test, head);
+ head->pp_recycle = 1;
+ skb_put(head, 32);
+ page_pool_ref_netmem(pp_nm);
+ skb_fill_netmem_desc(head, 0, niov_nm, 0, 32);
+ napi_pp_get_page(niov_nm);
+ head->len += 32;
+ head->data_len += 32;
+
+ seg = alloc_skb(0, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, seg);
+ skb_put(seg, skb_tailroom(seg));
+ KUNIT_EXPECT_EQ(test, skb_zerocopy(seg, head, head->len, 0), -EFAULT);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&pp_page->pp_ref_count),
+ base_pp_ref + 1);
+ skb_shinfo(seg)->nr_frags = 0;
+ consume_skb(seg);
+ consume_skb(head);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&pp_page->pp_ref_count),
+ base_pp_ref);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&niov.desc.pp_ref_count), 2L);
+
+ /* 7. skb_zerocopy -ENOMEM rollback when skb_orphan_frags() fails on
+ * shared zerocopy skb drops pp_ref_count via skb_frag_unref(to, 0).
+ */
+ head = build_skb(page_address(pp_page), PAGE_SIZE);
+ KUNIT_ASSERT_NOT_NULL(test, head);
+ head->pp_recycle = 1;
+ skb_put(head, 32);
+ page_pool_ref_netmem(pp_nm);
+ skb_zcopy_init(head, &uarg);
+ skb_get(head);
+
+ seg = alloc_skb(0, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, seg);
+ skb_put(seg, skb_tailroom(seg));
+ KUNIT_EXPECT_EQ(test, skb_zerocopy(seg, head, head->len, 0), -ENOMEM);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&pp_page->pp_ref_count),
+ base_pp_ref + 1);
+ KUNIT_EXPECT_EQ(test, page_ref_count(pp_page), base_pp_page_ref);
+ skb_shinfo(seg)->nr_frags = 0;
+ consume_skb(seg);
+ consume_skb(head);
+ skb_shinfo(head)->flags &= ~SKBFL_ZEROCOPY_ENABLE;
+ consume_skb(head);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&pp_page->pp_ref_count),
+ base_pp_ref);
+
+ page_pool_put_full_page(pool, pp_page, false);
+ page_pool_put_full_page(pool, pp_page, false);
+ __free_page(reg_page);
+ netmem_clear_pp_magic(niov_nm);
+ netmem_set_pp(niov_nm, NULL);
+ page_pool_destroy(pool);
+ percpu_ref_exit(&binding.ref);
+}
+
+static void netmem_ref_test_try_coalesce(struct kunit *test)
+{
+ struct page_pool_params pp_params = {
+ .order = 0,
+ .pool_size = 4,
+ .nid = NUMA_NO_NODE,
+ };
+ struct sk_buff *to, *from, *clone;
+ struct net_devmem_dmabuf_binding binding = {};
+ struct net_iov niov = {};
+ struct page *pp_page, *reg_page;
+ netmem_ref pp_nm, niov_nm;
+ struct page_pool *pool;
+ long base_pp_ref, base_iov_ref;
+ int base_pp_page_ref, base_reg_ref;
+ bool fragstolen;
+ int delta;
+
+ pool = page_pool_create(&pp_params);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(pool));
+ pp_page = page_pool_dev_alloc_pages(pool);
+ KUNIT_ASSERT_NOT_NULL(test, pp_page);
+ pp_nm = page_to_netmem(pp_page);
+ page_pool_ref_netmem(pp_nm);
+
+ reg_page = alloc_page(GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, reg_page);
+
+ KUNIT_ASSERT_EQ(test,
+ percpu_ref_init(&binding.ref, dummy_percpu_ref_release,
+ PERCPU_REF_INIT_ATOMIC, GFP_KERNEL),
+ 0);
+ net_iov_init(&niov, &binding.area, NET_IOV_DMABUF);
+ niov_nm = net_iov_to_netmem(&niov);
+ netmem_set_pp(niov_nm, pool);
+ netmem_or_pp_magic(niov_nm, PP_SIGNATURE);
+ atomic_long_set(&niov.desc.pp_ref_count, 2);
+
+ base_pp_ref = atomic_long_read(&pp_page->pp_ref_count);
+ base_pp_page_ref = page_ref_count(pp_page);
+ base_reg_ref = page_ref_count(reg_page);
+ base_iov_ref = atomic_long_read(&binding.ref.data->count);
+
+ /* 1. Uncloned pp_recycle=1 from: frags stolen without extra ref */
+ to = alloc_skb(0, GFP_KERNEL);
+ from = alloc_skb(0, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, to);
+ KUNIT_ASSERT_NOT_NULL(test, from);
+ skb_put(to, skb_tailroom(to));
+ to->pp_recycle = 1;
+ from->pp_recycle = 1;
+ skb_add_rx_frag(from, 0, pp_page, 0, 64, PAGE_SIZE);
+ page_pool_ref_netmem(pp_nm);
+ KUNIT_EXPECT_TRUE(test, skb_try_coalesce(to, from, &fragstolen,
+ &delta));
+ KUNIT_EXPECT_EQ(test, skb_shinfo(from)->nr_frags, 0);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&pp_page->pp_ref_count),
+ base_pp_ref + 1);
+ consume_skb(from);
+ consume_skb(to);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&pp_page->pp_ref_count),
+ base_pp_ref);
+
+ /* 2. Cloned pp_recycle=1 from (pp_page + non-PP reg_page fallback):
+ * skb_frag_ref(from, i) increments pp_ref_count for pp_page and
+ * page_ref_count for reg_page.
+ */
+ to = alloc_skb(0, GFP_KERNEL);
+ from = alloc_skb(0, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, to);
+ KUNIT_ASSERT_NOT_NULL(test, from);
+ skb_put(to, skb_tailroom(to));
+ to->pp_recycle = 1;
+ from->pp_recycle = 1;
+ skb_add_rx_frag(from, 0, pp_page, 0, 32, PAGE_SIZE);
+ page_pool_ref_netmem(pp_nm);
+ skb_add_rx_frag(from, 1, reg_page, 0, 32, PAGE_SIZE);
+ get_page(reg_page);
+
+ clone = skb_clone(from, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, clone);
+ KUNIT_EXPECT_TRUE(test, skb_try_coalesce(to, from, &fragstolen,
+ &delta));
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&pp_page->pp_ref_count),
+ base_pp_ref + 2);
+ KUNIT_EXPECT_EQ(test, page_ref_count(pp_page), base_pp_page_ref);
+ KUNIT_EXPECT_EQ(test, page_ref_count(reg_page), base_reg_ref + 2);
+ consume_skb(from);
+ consume_skb(clone);
+ consume_skb(to);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&pp_page->pp_ref_count),
+ base_pp_ref);
+ KUNIT_EXPECT_EQ(test, page_ref_count(pp_page), base_pp_page_ref);
+ KUNIT_EXPECT_EQ(test, page_ref_count(reg_page), base_reg_ref);
+
+ /* 3. Cloned unreadable pp_recycle=1 net_iov skb: skb_try_coalesce
+ * increments niov.desc.pp_ref_count via skb_frag_ref(from, i).
+ */
+ to = alloc_skb(0, GFP_KERNEL);
+ from = alloc_skb(0, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, to);
+ KUNIT_ASSERT_NOT_NULL(test, from);
+ skb_put(to, skb_tailroom(to));
+ to->pp_recycle = 1;
+ to->unreadable = 1;
+ from->pp_recycle = 1;
+ skb_add_rx_frag_netmem(from, 0, niov_nm, 0, 64, PAGE_SIZE);
+ napi_pp_get_page(niov_nm);
+
+ clone = skb_clone(from, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, clone);
+ KUNIT_EXPECT_TRUE(test, skb_try_coalesce(to, from, &fragstolen,
+ &delta));
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&niov.desc.pp_ref_count), 4L);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&binding.ref.data->count),
+ base_iov_ref);
+ consume_skb(from);
+ consume_skb(clone);
+ consume_skb(to);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&niov.desc.pp_ref_count), 2L);
+
+ page_pool_put_full_page(pool, pp_page, false);
+ page_pool_put_full_page(pool, pp_page, false);
+ __free_page(reg_page);
+ netmem_clear_pp_magic(niov_nm);
+ netmem_set_pp(niov_nm, NULL);
+ page_pool_destroy(pool);
+ percpu_ref_exit(&binding.ref);
+}
+
+#ifdef CONFIG_INET
+static void netmem_ref_test_tcp_devmem_recv(struct kunit *test)
+{
+ char cmsg_buf[CMSG_SPACE(sizeof(struct dmabuf_cmsg))]
+ __aligned(__alignof__(struct cmsghdr)) = {};
+ struct page_pool_params pp_params = {
+ .order = 0,
+ .pool_size = 4,
+ .nid = NUMA_NO_NODE,
+ };
+ struct net_devmem_dmabuf_binding binding = {};
+ struct msghdr msg = {};
+ struct dmabuf_token token;
+ struct dmabuf_cmsg *cmsg;
+ struct net_iov niov = {};
+ struct page_pool *pool;
+ struct cmsghdr *cm;
+ struct socket *sock;
+ struct sk_buff *skb;
+ struct kvec iov = {};
+ netmem_ref niov_nm;
+ long base_iov_ref;
+ int ret;
+
+ KUNIT_ASSERT_EQ(test,
+ sock_create_kern(&init_net, AF_INET, SOCK_STREAM,
+ IPPROTO_TCP, &sock),
+ 0);
+ sock->sk->sk_state = TCP_ESTABLISHED;
+ inet_csk(sock->sk)->icsk_ack.rcv_mss = 64;
+ tcp_sk(sock->sk)->copied_seq = 0;
+
+ pool = page_pool_create(&pp_params);
+ if (IS_ERR(pool)) {
+ sock->sk->sk_state = TCP_CLOSE;
+ sock_release(sock);
+ KUNIT_ASSERT_FALSE(test, IS_ERR(pool));
+ }
+
+ ret = percpu_ref_init(&binding.ref, dummy_percpu_ref_release,
+ PERCPU_REF_INIT_ATOMIC, GFP_KERNEL);
+ if (ret) {
+ page_pool_destroy(pool);
+ sock->sk->sk_state = TCP_CLOSE;
+ sock_release(sock);
+ KUNIT_ASSERT_EQ(test, ret, 0);
+ }
+ binding.area.niovs = &niov;
+ net_iov_init(&niov, &binding.area, NET_IOV_DMABUF);
+ niov_nm = net_iov_to_netmem(&niov);
+ netmem_set_pp(niov_nm, pool);
+ netmem_or_pp_magic(niov_nm, PP_SIGNATURE);
+ atomic_long_set(&niov.desc.pp_ref_count, 2);
+ base_iov_ref = atomic_long_read(&binding.ref.data->count);
+
+ /* 1. Receive via tcp_recvmsg_dmabuf -> SO_DEVMEM_DONTNEED release */
+ skb = alloc_skb(0, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, skb);
+ skb->pp_recycle = 1;
+ skb_add_rx_frag_netmem(skb, 0, niov_nm, 0, 64, PAGE_SIZE);
+ napi_pp_get_page(niov_nm);
+ TCP_SKB_CB(skb)->seq = 0;
+ TCP_SKB_CB(skb)->end_seq = 64;
+ skb_queue_tail(&sock->sk->sk_receive_queue, skb);
+
+ msg.msg_control = cmsg_buf;
+ msg.msg_controllen = sizeof(cmsg_buf);
+ ret = kernel_recvmsg(sock, &msg, &iov, 0, 64,
+ MSG_SOCK_DEVMEM | MSG_DONTWAIT);
+ KUNIT_ASSERT_EQ(test, ret, 64);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&niov.desc.pp_ref_count), 3L);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&binding.ref.data->count),
+ base_iov_ref);
+
+ cm = (struct cmsghdr *)cmsg_buf;
+ cmsg = CMSG_DATA(cm);
+ token.token_start = cmsg->frag_token;
+ token.token_count = 1;
+ KUNIT_EXPECT_EQ(test,
+ sock_setsockopt(sock, SOL_SOCKET, SO_DEVMEM_DONTNEED,
+ KERNEL_SOCKPTR(&token), sizeof(token)),
+ 1);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&niov.desc.pp_ref_count), 2L);
+
+ /* 2. Receive second packet & destroy socket to test
+ * tcp_release_user_frags() -> napi_pp_put_page().
+ */
+ skb = alloc_skb(0, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, skb);
+ skb->pp_recycle = 1;
+ skb_add_rx_frag_netmem(skb, 0, niov_nm, 0, 64, PAGE_SIZE);
+ napi_pp_get_page(niov_nm);
+ TCP_SKB_CB(skb)->seq = 64;
+ TCP_SKB_CB(skb)->end_seq = 128;
+ skb_queue_tail(&sock->sk->sk_receive_queue, skb);
+
+ msg.msg_control = cmsg_buf;
+ msg.msg_controllen = sizeof(cmsg_buf);
+ ret = kernel_recvmsg(sock, &msg, &iov, 0, 64,
+ MSG_SOCK_DEVMEM | MSG_DONTWAIT);
+ KUNIT_ASSERT_EQ(test, ret, 64);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&niov.desc.pp_ref_count), 3L);
+
+ sock->sk->sk_state = TCP_CLOSE;
+ sock_release(sock);
+ KUNIT_EXPECT_EQ(test, atomic_long_read(&niov.desc.pp_ref_count), 2L);
+
+ netmem_clear_pp_magic(niov_nm);
+ netmem_set_pp(niov_nm, NULL);
+ page_pool_destroy(pool);
+ percpu_ref_exit(&binding.ref);
+}
+#endif
+#endif
+
static struct kunit_case net_test_cases[] = {
KUNIT_CASE_PARAM(gso_test_func, gso_test_gen_params),
KUNIT_CASE_PARAM(ip_tunnel_flags_test_run,
ip_tunnel_flags_test_gen_params),
+ KUNIT_CASE(netmem_ref_test_page),
+#ifdef CONFIG_PAGE_POOL
+ KUNIT_CASE(netmem_ref_test_pp_page),
+#endif
+#if defined(CONFIG_PAGE_POOL) && defined(CONFIG_NET_DEVMEM)
+ KUNIT_CASE(netmem_ref_test_net_iov),
+ KUNIT_CASE(netmem_ref_test_release_data),
+ KUNIT_CASE(netmem_ref_test_skb_segment),
+ KUNIT_CASE(netmem_ref_test_try_coalesce),
+#ifdef CONFIG_INET
+ KUNIT_CASE(netmem_ref_test_tcp_devmem_recv),
+#endif
+#endif
{ },
};
@@ -471,4 +1692,5 @@ static struct kunit_suite net_test_suite = {
kunit_test_suite(net_test_suite);
MODULE_DESCRIPTION("KUnit tests for networking core");
+MODULE_IMPORT_NS("EXPORTED_FOR_KUNIT_TESTING");
MODULE_LICENSE("GPL");
diff --git a/net/core/skbuff.c b/net/core/skbuff.c
index 93c2f440b9ec..b79d8b702a0b 100644
--- a/net/core/skbuff.c
+++ b/net/core/skbuff.c
@@ -65,6 +65,7 @@
#include <linux/kcov.h>
#include <linux/iov_iter.h>
#include <linux/crc32.h>
+#include <kunit/visibility.h>
#include <net/protocol.h>
#include <net/dst.h>
@@ -1026,6 +1027,15 @@ int skb_cow_data_for_xdp(struct page_pool *pool, struct sk_buff **pskb,
EXPORT_SYMBOL(skb_cow_data_for_xdp);
#if IS_ENABLED(CONFIG_PAGE_POOL)
+/**
+ * napi_pp_put_page - release a page_pool reference on a netmem
+ * @netmem: netmem to unreference
+ *
+ * Drops a page_pool reference (pp_ref_count) on @netmem if it belongs to a
+ * page_pool. Counterpart to napi_pp_get_page().
+ *
+ * Return: true if @netmem belongs to a page_pool, false otherwise.
+ */
bool napi_pp_put_page(netmem_ref netmem)
{
netmem = netmem_compound_head(netmem);
@@ -1038,44 +1048,35 @@ bool napi_pp_put_page(netmem_ref netmem)
return true;
}
EXPORT_SYMBOL(napi_pp_put_page);
-#endif
-
-static bool skb_pp_recycle(struct sk_buff *skb, void *data)
-{
- if (!IS_ENABLED(CONFIG_PAGE_POOL) || !skb->pp_recycle)
- return false;
- return napi_pp_put_page(page_to_netmem(virt_to_page(data)));
-}
/**
- * skb_pp_frag_ref() - Increase fragment references of a page pool aware skb
- * @skb: page pool aware skb
- *
- * Increase the fragment reference count (pp_ref_count) of a skb. This is
- * intended to gain fragment references only for page pool aware skbs,
- * i.e. when skb->pp_recycle is true, and not for fragments in a
- * non-pp-recycling skb. It has a fallback to increase references on normal
- * pages, as page pool aware skbs may also have normal page fragments.
+ * napi_pp_get_page - acquire a page_pool reference on a netmem
+ * @netmem: netmem to reference
+ *
+ * Acquires a page_pool reference (pp_ref_count) on @netmem if it belongs to a
+ * page_pool. Counterpart to napi_pp_put_page().
+ *
+ * Return: true if @netmem belongs to a page_pool, false otherwise.
*/
-static int skb_pp_frag_ref(struct sk_buff *skb)
+bool napi_pp_get_page(netmem_ref netmem)
{
- struct skb_shared_info *shinfo;
- netmem_ref head_netmem;
- int i;
+ netmem = netmem_compound_head(netmem);
- if (!skb->pp_recycle)
- return -EINVAL;
+ if (unlikely(!netmem_is_pp(netmem)))
+ return false;
- shinfo = skb_shinfo(skb);
+ page_pool_ref_netmem(netmem);
- for (i = 0; i < shinfo->nr_frags; i++) {
- head_netmem = netmem_compound_head(shinfo->frags[i].netmem);
- if (likely(netmem_is_pp(head_netmem)))
- page_pool_ref_netmem(head_netmem);
- else
- page_ref_inc(netmem_to_page(head_netmem));
- }
- return 0;
+ return true;
+}
+EXPORT_SYMBOL(napi_pp_get_page);
+#endif
+
+static bool skb_pp_recycle(struct sk_buff *skb, void *data)
+{
+ if (!IS_ENABLED(CONFIG_PAGE_POOL) || !skb->pp_recycle)
+ return false;
+ return napi_pp_put_page(page_to_netmem(virt_to_page(data)));
}
static void skb_kfree_head(void *head)
@@ -1102,7 +1103,7 @@ static void skb_release_data(struct sk_buff *skb, enum skb_drop_reason reason)
int i;
if (!skb_data_unref(skb, shinfo))
- goto exit;
+ return;
if (skb_zcopy(skb)) {
bool skip_unref = shinfo->flags & SKBFL_MANAGED_FRAG_REFS;
@@ -1113,24 +1114,14 @@ static void skb_release_data(struct sk_buff *skb, enum skb_drop_reason reason)
}
for (i = 0; i < shinfo->nr_frags; i++)
- __skb_frag_unref(&shinfo->frags[i], skb->pp_recycle);
+ skb_netmem_unref(skb_frag_netmem(&shinfo->frags[i]),
+ skb->pp_recycle);
free_head:
if (shinfo->frag_list)
kfree_skb_list_reason(shinfo->frag_list, reason);
skb_free_head(skb);
-exit:
- /* When we clone an SKB we copy the reycling bit. The pp_recycle
- * bit is only set on the head though, so in order to avoid races
- * while trying to recycle fragments on __skb_frag_unref() we need
- * to make one SKB responsible for triggering the recycle path.
- * So disable the recycling bit if an SKB is cloned and we have
- * additional references to the fragmented part of the SKB.
- * Eventually the last SKB will have the recycling bit set and it's
- * dataref set to 0, which will trigger the recycling
- */
- skb->pp_recycle = 0;
}
/*
@@ -2260,9 +2251,10 @@ struct sk_buff *__pskb_copy_fclone(struct sk_buff *skb, int headroom,
n = NULL;
goto out;
}
+ n->pp_recycle = skb->pp_recycle;
for (i = 0; i < skb_shinfo(skb)->nr_frags; i++) {
skb_shinfo(n)->frags[i] = skb_shinfo(skb)->frags[i];
- skb_frag_ref(skb, i);
+ skb_frag_ref(n, i);
}
skb_shinfo(n)->nr_frags = i;
skb_shinfo(n)->flags |= skb_shinfo(skb)->flags & SKBFL_SHARED_FRAG;
@@ -3926,6 +3918,7 @@ skb_zerocopy(struct sk_buff *to, struct sk_buff *from, int len, int hlen)
if (len <= skb_tailroom(to))
return skb_copy_bits(from, 0, skb_put(to, len), len);
+ to->pp_recycle = from->pp_recycle;
if (hlen) {
ret = skb_copy_bits(from, 0, skb_put(to, hlen), hlen);
if (unlikely(ret))
@@ -3938,14 +3931,14 @@ skb_zerocopy(struct sk_buff *to, struct sk_buff *from, int len, int hlen)
offset = from->data - (unsigned char *)page_address(page);
__skb_fill_netmem_desc(to, 0, page_to_netmem(page),
offset, plen);
- get_page(page);
+ skb_frag_ref(to, 0);
j = 1;
len -= plen;
}
}
if (!skb_frags_readable(from) && j > 0 && len) {
- put_page(page);
+ skb_frag_unref(to, 0);
return -EFAULT;
}
@@ -3953,7 +3946,7 @@ skb_zerocopy(struct sk_buff *to, struct sk_buff *from, int len, int hlen)
if (unlikely(skb_orphan_frags(from, GFP_ATOMIC))) {
if (j > 0)
- put_page(page);
+ skb_frag_unref(to, 0);
return -ENOMEM;
}
skb_zerocopy_clone(to, from, GFP_ATOMIC);
@@ -4265,7 +4258,7 @@ static inline void skb_split_no_header(struct sk_buff *skb,
* where splitting is expensive.
* 2. Split is accurately. We make this.
*/
- skb_frag_ref(skb, i);
+ skb_frag_ref(skb1, k);
skb_frag_off_add(&skb_shinfo(skb1)->frags[0], len - pos);
skb_frag_size_sub(&skb_shinfo(skb1)->frags[0], len - pos);
skb_frag_size_set(&skb_shinfo(skb)->frags[i], len - pos);
@@ -4296,6 +4289,7 @@ void skb_split(struct sk_buff *skb, struct sk_buff *skb1, const u32 len)
skb_shinfo(skb1)->flags |= skb_shinfo(skb)->flags & zc_flags;
skb_zerocopy_clone(skb1, skb, 0);
+ skb1->pp_recycle = skb->pp_recycle;
if (len < pos) /* Split line is inside header. */
skb_split_inside_header(skb, skb1, len, pos);
else /* Second chunk has no header, nothing to copy. */
@@ -4356,8 +4350,8 @@ int skb_shift(struct sk_buff *tgt, struct sk_buff *skb, int shiftlen)
/* Actual merge is delayed until the point when we know we can
* commit all, so that we don't have to undo partial changes
*/
- if (!skb_can_coalesce(tgt, to, skb_frag_page(fragfrom),
- skb_frag_off(fragfrom))) {
+ if (!skb_can_coalesce_netmem(tgt, to, skb_frag_netmem(fragfrom),
+ skb_frag_off(fragfrom))) {
merge = -1;
} else {
merge = to - 1;
@@ -4404,8 +4398,8 @@ int skb_shift(struct sk_buff *tgt, struct sk_buff *skb, int shiftlen)
to++;
} else {
- __skb_frag_ref(fragfrom);
skb_frag_page_copy(fragto, fragfrom);
+ skb_frag_ref(tgt, to);
skb_frag_off_copy(fragto, fragfrom);
skb_frag_size_set(fragto, todo);
@@ -4434,7 +4428,7 @@ int skb_shift(struct sk_buff *tgt, struct sk_buff *skb, int shiftlen)
fragto = &skb_shinfo(tgt)->frags[merge];
skb_frag_size_add(fragto, skb_frag_size(fragfrom));
- __skb_frag_unref(fragfrom, skb->pp_recycle);
+ skb_frag_unref(skb, 0);
}
/* Reposition in the original skb */
@@ -4457,6 +4451,7 @@ onlymerged:
return shiftlen;
}
+EXPORT_SYMBOL_IF_KUNIT(skb_shift);
/**
* skb_prepare_seq_read - Prepare a sequential read of skb data
@@ -5085,6 +5080,16 @@ normal:
while (pos < offset + len) {
if (i >= nfrags) {
+ if (skb_shinfo(nskb)->nr_frags &&
+ nskb->pp_recycle != list_skb->pp_recycle) {
+ nskb->data_len = pos - offset - hsize;
+ nskb->len += nskb->data_len;
+ nskb->truesize += nskb->data_len;
+ if (__skb_linearize(nskb))
+ goto err;
+ hsize = pos - offset;
+ nskb_frag = skb_shinfo(nskb)->frags;
+ }
if (skb_orphan_frags(list_skb, GFP_ATOMIC) ||
skb_zerocopy_clone(nskb, list_skb,
GFP_ATOMIC))
@@ -5127,8 +5132,10 @@ normal:
goto err;
}
+ if (!skb_shinfo(nskb)->nr_frags)
+ nskb->pp_recycle = frag_skb->pp_recycle;
*nskb_frag = (i < 0) ? skb_head_frag_to_page_desc(frag_skb) : *frag;
- __skb_frag_ref(nskb_frag);
+ skb_frag_ref(nskb, skb_shinfo(nskb)->nr_frags);
size = skb_frag_size(nskb_frag);
if (pos < offset) {
@@ -6355,10 +6362,8 @@ bool skb_try_coalesce(struct sk_buff *to, struct sk_buff *from,
/* if the skb is not cloned this does nothing
* since we set nr_frags to 0.
*/
- if (skb_pp_frag_ref(from)) {
- for (i = 0; i < from_shinfo->nr_frags; i++)
- __skb_frag_ref(&from_shinfo->frags[i]);
- }
+ for (i = 0; i < from_shinfo->nr_frags; i++)
+ skb_frag_ref(from, i);
to->truesize += delta;
to->len += len;
@@ -7637,23 +7642,39 @@ bool csum_and_copy_from_iter_full(void *addr, size_t bytes,
}
EXPORT_SYMBOL(csum_and_copy_from_iter_full);
-void __get_netmem(netmem_ref netmem)
+/**
+ * get_net_iov - acquire a non-page_pool reference on a net_iov
+ * @niov: net_iov to reference
+ *
+ * Acquires a non-pp backing reference (binding->ref) on @niov regardless of
+ * page_pool membership, warning if @niov does not support non-pp references.
+ * Counterpart to put_net_iov().
+ */
+void get_net_iov(struct net_iov *niov)
{
- struct net_iov *niov = netmem_to_net_iov(netmem);
-
if (net_is_devmem_iov(niov))
- net_devmem_get_net_iov(netmem_to_net_iov(netmem));
+ net_devmem_get_net_iov(niov);
+ else
+ DEBUG_NET_WARN_ON_ONCE(true);
}
-EXPORT_SYMBOL(__get_netmem);
+EXPORT_SYMBOL(get_net_iov);
-void __put_netmem(netmem_ref netmem)
+/**
+ * put_net_iov - release a non-page_pool reference on a net_iov
+ * @niov: net_iov to unreference
+ *
+ * Drops a non-pp backing reference (binding->ref) on @niov regardless of
+ * page_pool membership, warning if @niov does not support non-pp references.
+ * Counterpart to get_net_iov().
+ */
+void put_net_iov(struct net_iov *niov)
{
- struct net_iov *niov = netmem_to_net_iov(netmem);
-
if (net_is_devmem_iov(niov))
- net_devmem_put_net_iov(netmem_to_net_iov(netmem));
+ net_devmem_put_net_iov(niov);
+ else
+ DEBUG_NET_WARN_ON_ONCE(true);
}
-EXPORT_SYMBOL(__put_netmem);
+EXPORT_SYMBOL(put_net_iov);
struct vlan_type_depth __vlan_get_protocol_offset(const struct sk_buff *skb,
__be16 type,
diff --git a/net/ipv4/esp4.c b/net/ipv4/esp4.c
index e76db5817e78..ae701561d9f1 100644
--- a/net/ipv4/esp4.c
+++ b/net/ipv4/esp4.c
@@ -117,8 +117,8 @@ static void esp_ssg_unref(struct xfrm_state *x, void *tmp, struct sk_buff *skb,
struct scatterlist *src = already_unref ? esp_req_sg(aead, req) : req->src;
for (sg = sg_next(src); sg; sg = sg_next(sg))
- skb_page_unref(page_to_netmem(sg_page(sg)),
- skb->pp_recycle);
+ skb_netmem_unref(page_to_netmem(sg_page(sg)),
+ skb->pp_recycle);
}
}
diff --git a/net/ipv4/tcp.c b/net/ipv4/tcp.c
index f4c9101e5996..99d7237a623d 100644
--- a/net/ipv4/tcp.c
+++ b/net/ipv4/tcp.c
@@ -254,6 +254,7 @@
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/skbuff.h>
+#include <linux/skbuff_ref.h>
#include <linux/splice.h>
#include <linux/net.h>
#include <linux/socket.h>
@@ -2559,6 +2560,7 @@ static int tcp_recvmsg_dmabuf(struct sock *sk, const struct sk_buff *skb,
*/
for (i = 0; i < skb_shinfo(skb)->nr_frags; i++) {
skb_frag_t *frag = &skb_shinfo(skb)->frags[i];
+ netmem_ref netmem = skb_frag_netmem(frag);
struct net_iov *niov;
u64 frag_offset;
int end;
@@ -2612,8 +2614,8 @@ static int tcp_recvmsg_dmabuf(struct sock *sk, const struct sk_buff *skb,
if (err)
goto out;
- atomic_long_inc(&niov->desc.pp_ref_count);
- tcp_xa_pool.netmems[tcp_xa_pool.idx++] = skb_frag_netmem(frag);
+ WARN_ON_ONCE(!napi_pp_get_page(netmem));
+ tcp_xa_pool.netmems[tcp_xa_pool.idx++] = netmem;
sent += copy;
diff --git a/net/ipv6/esp6.c b/net/ipv6/esp6.c
index b1c9b36f76dc..d654f0f44238 100644
--- a/net/ipv6/esp6.c
+++ b/net/ipv6/esp6.c
@@ -134,8 +134,8 @@ static void esp_ssg_unref(struct xfrm_state *x, void *tmp, struct sk_buff *skb,
struct scatterlist *src = already_unref ? esp_req_sg(aead, req) : req->src;
for (sg = sg_next(src); sg; sg = sg_next(sg))
- skb_page_unref(page_to_netmem(sg_page(sg)),
- skb->pp_recycle);
+ skb_netmem_unref(page_to_netmem(sg_page(sg)),
+ skb->pp_recycle);
}
}
diff --git a/net/tls/tls_device.c b/net/tls/tls_device.c
index f11d0528fc43..b333a9a72cda 100644
--- a/net/tls/tls_device.c
+++ b/net/tls/tls_device.c
@@ -143,7 +143,7 @@ static void destroy_record(struct tls_record_info *record)
int i;
for (i = 0; i < record->num_frags; i++)
- __skb_frag_unref(&record->frags[i], false);
+ put_netmem(skb_frag_netmem(&record->frags[i]));
kfree(record);
}
diff --git a/net/tls/tls_device_fallback.c b/net/tls/tls_device_fallback.c
index 3b7d0ab2bcf1..35cc510db432 100644
--- a/net/tls/tls_device_fallback.c
+++ b/net/tls/tls_device_fallback.c
@@ -256,7 +256,7 @@ static int fill_sg_in(struct scatterlist *sg_in,
for (i = 0; remaining > 0; i++) {
skb_frag_t *frag = &record->frags[i];
- __skb_frag_ref(frag);
+ get_netmem(skb_frag_netmem(frag));
sg_set_page(sg_in + i, skb_frag_page(frag),
skb_frag_size(frag), skb_frag_off(frag));
diff --git a/net/tls/tls_strp.c b/net/tls/tls_strp.c
index 6cc222008d95..53d3256d0114 100644
--- a/net/tls/tls_strp.c
+++ b/net/tls/tls_strp.c
@@ -197,7 +197,7 @@ static void tls_strp_flush_anchor_copy(struct tls_strparser *strp)
DEBUG_NET_WARN_ON_ONCE(atomic_read(&shinfo->dataref) != 1);
for (i = 0; i < shinfo->nr_frags; i++)
- __skb_frag_unref(&shinfo->frags[i], false);
+ skb_frag_unref(strp->anchor, i);
shinfo->nr_frags = 0;
if (strp->copy_mode) {
kfree_skb_list(shinfo->frag_list);
diff --git a/net/xfrm/xfrm_iptfs.c b/net/xfrm/xfrm_iptfs.c
index 6920940a35b4..103cf7c5dbfb 100644
--- a/net/xfrm/xfrm_iptfs.c
+++ b/net/xfrm/xfrm_iptfs.c
@@ -486,8 +486,7 @@ static int iptfs_skb_add_frags(struct sk_buff *skb,
tofrag->len -= offset;
offset = 0;
}
- __skb_frag_ref(tofrag);
- shinfo->nr_frags++;
+ skb_frag_ref(skb, shinfo->nr_frags++);
shinfo->flags |= SKBFL_SHARED_FRAG;
/* see if we are done */