All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH net-next v2 0/4] net: prevent mixing incompatible memory providers in skbs
@ 2026-11-02 23:18 Mina Almasry
  2026-11-02 23:18 ` [PATCH net-next v2 1/4] net: fix skb->unreadable propagation in skb_split() and skb_shift() Mina Almasry
                   ` (3 more replies)
  0 siblings, 4 replies; 8+ messages in thread
From: Mina Almasry @ 2026-11-02 23:18 UTC (permalink / raw)
  To: netdev, linux-kernel, Jakub Kicinski, Willem de Bruijn,
	Eric Dumazet, Mina Almasry, Kaiyuan Zhang, io-uring,
	Stanislav Fomichev, Paolo Abeni
  Cc: David S. Miller, Simon Horman, Pavel Begunkov, Jens Axboe,
	Stanislav Fomichev, Bobby Eshleman, Daniel Borkmann,
	Neal Cardwell, Kuniyuki Iwashima, David Ahern, Ido Schimmel

Enforce that all fragments in a nonlinear skb come from compatible
memory providers (and for devmem, the exact same binding).

Patch 1 fixes skb->unreadable propagation in skb_split() and skb_shift().
Patch 2 stores mp_priv directly inside struct net_iov.
Patch 3 adds skb_has_net_iovs(), skb_mem_provider_compat(), and
skb_frags_can_coalesce().
Patch 4 enforces provider/binding compatibility across TX, TCP, GRO,
IP defrag, skb_shift(), and skb_try_coalesce().

v1 -> v2:
- Generalize the check across TX, TCP collapse, GRO, IP defrag,
  skb_shift(), and skb_try_coalesce() by storing mp_priv directly in
  struct net_iov and introducing skb_frags_can_coalesce().
- Fix skb->unreadable propagation in skb_split() and skb_shift().
- Add KUnit coverage for cross-provider and cross-binding rejection.
- v1: https://lore.kernel.org/netdev/20261008034027.1403188-1-almasrymina@google.com/


Mina Almasry (4):
  net: fix skb->unreadable propagation in skb_split() and skb_shift()
  netmem: store memory provider private pointer directly in struct
    net_iov
  net: add skb_frags_can_coalesce() helper
  net: prevent mixing frags from different memory providers or bindings

 include/linux/skbuff.h   | 58 ++++++++++++++++++++++++++++++++++++++++
 include/net/netmem.h     | 15 +++++++++--
 include/net/tcp.h        |  5 ++--
 io_uring/zcrx.c          |  2 +-
 net/core/datagram.c      |  4 +--
 net/core/dev.c           | 14 +++-------
 net/core/devmem.c        |  3 ++-
 net/core/devmem.h        | 25 +++++++++++++++--
 net/core/gro.c           | 16 +++++------
 net/core/net_test.c      | 34 +++++++++++++++++------
 net/core/skbuff.c        | 34 ++++++++++++-----------
 net/ipv4/inet_fragment.c |  3 +++
 net/ipv4/tcp.c           |  2 +-
 net/ipv4/tcp_output.c    |  3 +++
 14 files changed, 163 insertions(+), 55 deletions(-)


base-commit: 4dd16e31135ea8437a720222df34e8745f385d6d
-- 
2.56.0.385.gac671c819a-goog


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [PATCH net-next v2 1/4] net: fix skb->unreadable propagation in skb_split() and skb_shift()
  2026-11-02 23:18 [PATCH net-next v2 0/4] net: prevent mixing incompatible memory providers in skbs Mina Almasry
@ 2026-11-02 23:18 ` Mina Almasry
  2026-11-03 22:42   ` Stanislav Fomichev
  2026-11-02 23:18 ` [PATCH net-next v2 2/4] netmem: store memory provider private pointer directly in struct net_iov Mina Almasry
                   ` (2 subsequent siblings)
  3 siblings, 1 reply; 8+ messages in thread
From: Mina Almasry @ 2026-11-02 23:18 UTC (permalink / raw)
  To: netdev, linux-kernel, Jakub Kicinski, Willem de Bruijn,
	Eric Dumazet, Mina Almasry, Kaiyuan Zhang, io-uring,
	Stanislav Fomichev, Paolo Abeni
  Cc: David S. Miller, Simon Horman, Pavel Begunkov, Jens Axboe,
	Stanislav Fomichev, Bobby Eshleman, Daniel Borkmann,
	Neal Cardwell, Kuniyuki Iwashima, David Ahern, Ido Schimmel

skb_split() and skb_shift() do not accurately maintain skb->unreadable
when moving fragments between skbs:

- skb_split_inside_header() moves all fragments out of skb into skb1
  (leaving skb with nr_frags == 0 and only host-readable linear memory),
  but leaves skb->unreadable set on skb.
- skb_split_no_header() unconditionally copies skb->unreadable to skb1
  even if 0 fragments were moved to skb1 (k == 0), and leaves
  skb->unreadable set on skb when splitting at skb_headlen(skb) moves
  all fragments into skb1 (!skb_shinfo(skb)->nr_frags).
- skb_shift() neither propagates skb->unreadable to target skb when
  shifting unreadable fragments into an empty skb, nor clears
  skb->unreadable after all fragments are shifted out of skb.

Fix all three cases so skb->unreadable accurately tracks whether the
remaining fragments in each skb are unreadable.

This issue is theoretical and was found by manual code inspection and
LLM review; it was not triggered in production. Verified with the
net_core KUnit test suite; not tested on real hardware.

Fixes: 65249feb6b3d ("net: add support for skbs with unreadable frags")
Signed-off-by: Mina Almasry <almasrymina@google.com>
---
 net/core/skbuff.c | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/net/core/skbuff.c b/net/core/skbuff.c
index 48f6764117243..dc761ceb4d204 100644
--- a/net/core/skbuff.c
+++ b/net/core/skbuff.c
@@ -4225,6 +4225,7 @@ static inline void skb_split_inside_header(struct sk_buff *skb,
 
 	skb_shinfo(skb1)->nr_frags = skb_shinfo(skb)->nr_frags;
 	skb1->unreadable	   = skb->unreadable;
+	skb->unreadable		   = 0;
 	skb_shinfo(skb)->nr_frags  = 0;
 	skb1->data_len		   = skb->data_len;
 	skb1->len		   += skb1->data_len;
@@ -4273,7 +4274,10 @@ static inline void skb_split_no_header(struct sk_buff *skb,
 	}
 	skb_shinfo(skb1)->nr_frags = k;
 
-	skb1->unreadable = skb->unreadable;
+	if (k)
+		skb1->unreadable = skb->unreadable;
+	if (!skb_shinfo(skb)->nr_frags)
+		skb->unreadable = 0;
 }
 
 /**
@@ -4447,6 +4451,9 @@ int skb_shift(struct sk_buff *tgt, struct sk_buff *skb, int shiftlen)
 	skb->ip_summed = CHECKSUM_PARTIAL;
 
 	skb_shinfo(tgt)->flags |= skb_shinfo(skb)->flags & SKBFL_SHARED_FRAG;
+	tgt->unreadable |= skb->unreadable;
+	if (!skb_shinfo(skb)->nr_frags && !skb_has_frag_list(skb))
+		skb->unreadable = 0;
 
 	skb_len_add(skb, -shiftlen);
 	skb_len_add(tgt, shiftlen);
-- 
2.56.0.385.gac671c819a-goog


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH net-next v2 2/4] netmem: store memory provider private pointer directly in struct net_iov
  2026-11-02 23:18 [PATCH net-next v2 0/4] net: prevent mixing incompatible memory providers in skbs Mina Almasry
  2026-11-02 23:18 ` [PATCH net-next v2 1/4] net: fix skb->unreadable propagation in skb_split() and skb_shift() Mina Almasry
@ 2026-11-02 23:18 ` Mina Almasry
  2026-11-03 22:42   ` Stanislav Fomichev
  2026-11-02 23:18 ` [PATCH net-next v2 3/4] net: add skb_frags_can_coalesce() helper Mina Almasry
  2026-11-02 23:18 ` [PATCH net-next v2 4/4] net: prevent mixing frags from different memory providers or bindings Mina Almasry
  3 siblings, 1 reply; 8+ messages in thread
From: Mina Almasry @ 2026-11-02 23:18 UTC (permalink / raw)
  To: netdev, linux-kernel, Jakub Kicinski, Willem de Bruijn,
	Eric Dumazet, Mina Almasry, Kaiyuan Zhang, io-uring,
	Stanislav Fomichev, Paolo Abeni
  Cc: David S. Miller, Simon Horman, Pavel Begunkov, Jens Axboe,
	Stanislav Fomichev, Bobby Eshleman, Daniel Borkmann,
	Neal Cardwell, Kuniyuki Iwashima, David Ahern, Ido Schimmel

Resolving the memory provider instance from a struct net_iov currently
chases niov->owner (struct net_iov_area) and then dereferences the
provider-specific container struct (such as net_devmem_dmabuf_binding
or io_zcrx_area->ifq).

Alias void *mp_priv over the unused _pp_mapping_pad slot inside struct
net_iov (keeping sizeof(struct net_iov) == 64 bytes unchanged) and populate
it in net_iov_init() for both TX and RX net_iovs so callers can obtain the
provider instance pointer in a single load from the net_iov cacheline.

Signed-off-by: Mina Almasry <almasrymina@google.com>
---
 include/net/netmem.h | 15 +++++++++++++--
 io_uring/zcrx.c      |  2 +-
 net/core/devmem.c    |  3 ++-
 net/core/devmem.h    |  3 +--
 net/core/net_test.c  | 10 +++++-----
 5 files changed, 22 insertions(+), 11 deletions(-)

diff --git a/include/net/netmem.h b/include/net/netmem.h
index fb693bc1702a4..c869be9c4d266 100644
--- a/include/net/netmem.h
+++ b/include/net/netmem.h
@@ -95,11 +95,20 @@ enum net_iov_type {
  *		supported.
  */
 struct net_iov {
-	struct netmem_desc desc;
+	union {
+		struct netmem_desc desc;
+		struct {
+			unsigned long _pad[3];
+			void *mp_priv;
+		};
+	};
 	enum net_iov_type type;
 	struct net_iov_area *owner;
 };
 
+static_assert(offsetof(struct net_iov, mp_priv) ==
+	      offsetof(struct netmem_desc, _pp_mapping_pad));
+
 struct net_iov_area {
 	/* Array of net_iovs for this area. */
 	struct net_iov *niovs;
@@ -120,10 +129,12 @@ static inline unsigned int net_iov_idx(const struct net_iov *niov)
  */
 static inline void net_iov_init(struct net_iov *niov,
 				struct net_iov_area *owner,
-				enum net_iov_type type)
+				enum net_iov_type type,
+				void *mp_priv)
 {
 	niov->owner = owner;
 	niov->type = type;
+	niov->mp_priv = mp_priv;
 }
 
 /* netmem */
diff --git a/io_uring/zcrx.c b/io_uring/zcrx.c
index 6d7abb1636e7f..6ec618d325606 100644
--- a/io_uring/zcrx.c
+++ b/io_uring/zcrx.c
@@ -562,7 +562,7 @@ static int __zcrx_create_area(struct io_zcrx_ifq *ifq,
 	for (i = 0; i < nr_iovs; i++) {
 		struct net_iov *niov = &area->nia.niovs[i];
 
-		net_iov_init(niov, &area->nia, NET_IOV_IOURING);
+		net_iov_init(niov, &area->nia, NET_IOV_IOURING, ifq);
 		area->freelist[i] = i;
 		atomic_set(&area->user_refs[i], 0);
 	}
diff --git a/net/core/devmem.c b/net/core/devmem.c
index 32fafbfa1359e..fe99c005cbc5e 100644
--- a/net/core/devmem.c
+++ b/net/core/devmem.c
@@ -323,7 +323,8 @@ net_devmem_bind_dmabuf(struct net_device *dev, void *vdev,
 		nr_niovs = len >> niov_shift;
 		for (i = 0; i < nr_niovs; i++, niov_idx++) {
 			niov = &binding->area.niovs[niov_idx];
-			net_iov_init(niov, &binding->area, NET_IOV_DMABUF);
+			net_iov_init(niov, &binding->area, NET_IOV_DMABUF,
+				     binding);
 			if (net_mp_niov_set_dma_addr(niov, dma_addr)) {
 				err = -EFAULT;
 				NL_SET_ERR_MSG(extack,
diff --git a/net/core/devmem.h b/net/core/devmem.h
index 8fa089776610a..942b8caf36e99 100644
--- a/net/core/devmem.h
+++ b/net/core/devmem.h
@@ -102,8 +102,7 @@ int net_devmem_bind_dmabuf_to_queue(struct net_device *dev, u32 rxq_idx,
 static inline struct net_devmem_dmabuf_binding *
 net_devmem_iov_binding(const struct net_iov *niov)
 {
-	return container_of(net_iov_owner(niov),
-			    struct net_devmem_dmabuf_binding, area);
+	return niov->mp_priv;
 }
 
 static inline u32 net_devmem_iov_binding_id(const struct net_iov *niov)
diff --git a/net/core/net_test.c b/net/core/net_test.c
index 2b5080c226636..63124dbf3c9a8 100644
--- a/net/core/net_test.c
+++ b/net/core/net_test.c
@@ -857,7 +857,7 @@ static void netmem_ref_test_net_iov(struct kunit *test)
 			      PERCPU_REF_INIT_ATOMIC, GFP_KERNEL);
 	KUNIT_ASSERT_EQ(test, err, 0);
 
-	net_iov_init(&niov, &binding.area, NET_IOV_DMABUF);
+	net_iov_init(&niov, &binding.area, NET_IOV_DMABUF, &binding);
 	netmem = net_iov_to_netmem(&niov);
 	base_ref = atomic_long_read(&binding.ref.data->count);
 
@@ -1145,7 +1145,7 @@ static void netmem_ref_test_release_data(struct kunit *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);
+	net_iov_init(&niov, &binding.area, NET_IOV_DMABUF, &binding);
 	netmem = net_iov_to_netmem(&niov);
 	netmem_set_pp(netmem, pool);
 	netmem_or_pp_magic(netmem, PP_SIGNATURE);
@@ -1206,7 +1206,7 @@ static void netmem_ref_test_skb_segment(struct kunit *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);
+	net_iov_init(&niov, &binding.area, NET_IOV_DMABUF, &binding);
 	niov_nm = net_iov_to_netmem(&niov);
 	netmem_set_pp(niov_nm, pool);
 	netmem_or_pp_magic(niov_nm, PP_SIGNATURE);
@@ -1508,7 +1508,7 @@ static void netmem_ref_test_try_coalesce(struct kunit *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);
+	net_iov_init(&niov, &binding.area, NET_IOV_DMABUF, &binding);
 	niov_nm = net_iov_to_netmem(&niov);
 	netmem_set_pp(niov_nm, pool);
 	netmem_or_pp_magic(niov_nm, PP_SIGNATURE);
@@ -1654,7 +1654,7 @@ static void netmem_ref_test_tcp_devmem_recv(struct kunit *test)
 		KUNIT_ASSERT_EQ(test, ret, 0);
 	}
 	binding.area.niovs = &niov;
-	net_iov_init(&niov, &binding.area, NET_IOV_DMABUF);
+	net_iov_init(&niov, &binding.area, NET_IOV_DMABUF, &binding);
 	niov_nm = net_iov_to_netmem(&niov);
 	netmem_set_pp(niov_nm, pool);
 	netmem_or_pp_magic(niov_nm, PP_SIGNATURE);
-- 
2.56.0.385.gac671c819a-goog


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH net-next v2 3/4] net: add skb_frags_can_coalesce() helper
  2026-11-02 23:18 [PATCH net-next v2 0/4] net: prevent mixing incompatible memory providers in skbs Mina Almasry
  2026-11-02 23:18 ` [PATCH net-next v2 1/4] net: fix skb->unreadable propagation in skb_split() and skb_shift() Mina Almasry
  2026-11-02 23:18 ` [PATCH net-next v2 2/4] netmem: store memory provider private pointer directly in struct net_iov Mina Almasry
@ 2026-11-02 23:18 ` Mina Almasry
  2026-11-03 22:43   ` Stanislav Fomichev
  2026-11-02 23:18 ` [PATCH net-next v2 4/4] net: prevent mixing frags from different memory providers or bindings Mina Almasry
  3 siblings, 1 reply; 8+ messages in thread
From: Mina Almasry @ 2026-11-02 23:18 UTC (permalink / raw)
  To: netdev, linux-kernel, Jakub Kicinski, Willem de Bruijn,
	Eric Dumazet, Mina Almasry, Kaiyuan Zhang, io-uring,
	Stanislav Fomichev, Paolo Abeni
  Cc: David S. Miller, Simon Horman, Pavel Begunkov, Jens Axboe,
	Stanislav Fomichev, Bobby Eshleman, Daniel Borkmann,
	Neal Cardwell, Kuniyuki Iwashima, David Ahern, Ido Schimmel

Add skb_has_net_iovs(skb), assuming skb->unreadable and net_iov-backed
frags are equivalent for now (with a comment noting the two can be
decoupled in the future if readable net_iovs are introduced).

Also add skb_mem_provider_compat() and skb_frags_can_coalesce() so all
multi-skb coalescing, shifting, and GRO paths can share a single check
across pp_recycle, skb_zcopy(), and fragment memory providers:
- empty 0-length skbs are compatible without mixing readable payload
- page vs net_iov is rejected via skb_has_net_iovs() (0 pointer chases)
- page vs page is allowed via !skb_has_net_iovs() (0 pointer chases)
- net_iov vs net_iov compares mp_priv (walking frag_list if nr_frags is 0)

Signed-off-by: Mina Almasry <almasrymina@google.com>
---
 include/linux/skbuff.h | 58 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 58 insertions(+)

diff --git a/include/linux/skbuff.h b/include/linux/skbuff.h
index 6787fcf6ac3e4..1e6e919c656d5 100644
--- a/include/linux/skbuff.h
+++ b/include/linux/skbuff.h
@@ -1882,6 +1882,16 @@ static inline bool skb_frags_readable(const struct sk_buff *skb)
 	return !skb->unreadable;
 }
 
+/* Return true if frags in this skb are backed by net_iovs.
+ * Assumes skb->unreadable and net_iov-backed frags are equivalent for now;
+ * the two can be decoupled in the future if readable net_iovs in skbs are
+ * introduced.
+ */
+static inline bool skb_has_net_iovs(const struct sk_buff *skb)
+{
+	return skb->unreadable;
+}
+
 static inline void skb_mark_not_on_list(struct sk_buff *skb)
 {
 	skb->next = NULL;
@@ -3763,6 +3773,54 @@ static inline void *skb_frag_address(const skb_frag_t *frag)
 	return page_address(skb_frag_page(frag)) + skb_frag_off(frag);
 }
 
+static inline void *skb_mem_provider_priv(const struct sk_buff *skb)
+{
+	netmem_ref netmem;
+
+	if (!skb_has_net_iovs(skb) || !skb_shinfo(skb)->nr_frags)
+		return NULL;
+
+	netmem = skb_frag_netmem(&skb_shinfo(skb)->frags[0]);
+	return netmem_to_net_iov(netmem)->mp_priv;
+}
+
+/* Return true if the memory providers of @skb1 and @skb2 are compatible
+ * for coalescing. For any struct page-backed provider (the buddy allocator
+ * or a future provider handing out struct pages), coalescing is assumed
+ * feasible. For net_iov-backed providers, the exact mp_priv pointer must
+ * match.
+ */
+static inline bool skb_mem_provider_compat(const struct sk_buff *skb1,
+					   const struct sk_buff *skb2)
+{
+	void *priv1, *priv2;
+
+	if (!skb1->len || !skb2->len)
+		return true;
+
+	if (skb_has_net_iovs(skb1) != skb_has_net_iovs(skb2))
+		return false;
+
+	if (!skb_has_net_iovs(skb1))
+		return true;
+
+	priv1 = skb_mem_provider_priv(skb1);
+	priv2 = skb_mem_provider_priv(skb2);
+	return priv1 && priv1 == priv2;
+}
+
+static inline bool skb_frags_can_coalesce(struct sk_buff *to,
+					  struct sk_buff *from)
+{
+	if (to->pp_recycle != from->pp_recycle)
+		return false;
+
+	if (skb_zcopy(to) || skb_zcopy(from))
+		return false;
+
+	return skb_mem_provider_compat(to, from);
+}
+
 /**
  * skb_frag_address_safe - gets the address of the data contained in a paged fragment
  * @frag: the paged fragment buffer
-- 
2.56.0.385.gac671c819a-goog


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH net-next v2 4/4] net: prevent mixing frags from different memory providers or bindings
  2026-11-02 23:18 [PATCH net-next v2 0/4] net: prevent mixing incompatible memory providers in skbs Mina Almasry
                   ` (2 preceding siblings ...)
  2026-11-02 23:18 ` [PATCH net-next v2 3/4] net: add skb_frags_can_coalesce() helper Mina Almasry
@ 2026-11-02 23:18 ` Mina Almasry
  3 siblings, 0 replies; 8+ messages in thread
From: Mina Almasry @ 2026-11-02 23:18 UTC (permalink / raw)
  To: netdev, linux-kernel, Jakub Kicinski, Willem de Bruijn,
	Eric Dumazet, Mina Almasry, Kaiyuan Zhang, io-uring,
	Stanislav Fomichev, Paolo Abeni
  Cc: David S. Miller, Simon Horman, Pavel Begunkov, Jens Axboe,
	Stanislav Fomichev, Bobby Eshleman, Daniel Borkmann,
	Neal Cardwell, Kuniyuki Iwashima, David Ahern, Ido Schimmel

Enforce that all fragments in a nonlinear skb come from compatible
memory providers (and for devmem, the exact same binding):

- Add net_devmem_skb_binding() and use it in
  validate_xmit_unreadable_skb(), zerocopy_fill_skb_from_devmem(), and
  tcp_sendmsg_locked() so frags from different devmem bindings or memory
  providers cannot be mixed on TX.
- Use skb_frags_can_coalesce() across skb_try_coalesce(), skb_shift(),
  skb_gro_receive(), and skb_gro_receive_list().
- Use skb_mem_provider_compat() in tcp_skb_can_collapse(),
  tcp_skb_can_collapse_rx(), and inet_frag_queue_insert().
- Reject coalescing linear head payloads into unreadable skbs in
  skb_try_coalesce(), skb_gro_receive(), and
  tcp_can_coalesce_send_queue_head().

This issue is theoretical and was found by manual code inspection and
LLM review; it was not triggered in production. Verified with the
net_core KUnit test suite; not tested on real hardware.

Fixes: 65249feb6b3d ("net: add support for skbs with unreadable frags")
Fixes: bd61848900bf ("net: devmem: Implement TX path")
Signed-off-by: Mina Almasry <almasrymina@google.com>
---
 include/net/tcp.h        |  5 +++--
 net/core/datagram.c      |  4 ++--
 net/core/dev.c           | 14 ++++----------
 net/core/devmem.h        | 22 ++++++++++++++++++++++
 net/core/gro.c           | 16 ++++++----------
 net/core/net_test.c      | 24 +++++++++++++++++++++---
 net/core/skbuff.c        | 25 ++++++++++---------------
 net/ipv4/inet_fragment.c |  3 +++
 net/ipv4/tcp.c           |  2 +-
 net/ipv4/tcp_output.c    |  3 +++
 10 files changed, 75 insertions(+), 43 deletions(-)

diff --git a/include/net/tcp.h b/include/net/tcp.h
index 3560adddf8215..07a222c0fe2d7 100644
--- a/include/net/tcp.h
+++ b/include/net/tcp.h
@@ -1268,14 +1268,15 @@ static inline bool tcp_skb_can_collapse(const struct sk_buff *to,
 		      mptcp_skb_can_collapse(to, from) &&
 		      !skb_cmp_decrypted(to, from) &&
 		      skb_pure_zcopy_same(to, from) &&
-		      skb_frags_readable(to) == skb_frags_readable(from));
+		      skb_mem_provider_compat(to, from));
 }
 
 static inline bool tcp_skb_can_collapse_rx(const struct sk_buff *to,
 					   const struct sk_buff *from)
 {
 	return likely(mptcp_skb_can_collapse(to, from) &&
-		      !skb_cmp_decrypted(to, from));
+		      !skb_cmp_decrypted(to, from) &&
+		      skb_mem_provider_compat(to, from));
 }
 
 /* Events passed to congestion control interface */
diff --git a/net/core/datagram.c b/net/core/datagram.c
index ecc30a6f83023..1e7db61e25a56 100644
--- a/net/core/datagram.c
+++ b/net/core/datagram.c
@@ -712,8 +712,8 @@ zerocopy_fill_skb_from_devmem(struct sk_buff *skb, struct iov_iter *from,
 	size_t virt_addr, size, off;
 	struct net_iov *niov;
 
-	if (i && skb_frags_readable(skb))
-		return -EFAULT;
+	if (skb->len && net_devmem_skb_binding(skb) != binding)
+		return -EMSGSIZE;
 
 	/* Devmem filling works by taking an IOVEC from the user where the
 	 * iov_addrs are interpreted as an offset in bytes into the dma-buf to
diff --git a/net/core/dev.c b/net/core/dev.c
index 3ac700d37e596..c1ea3b26a5a11 100644
--- a/net/core/dev.c
+++ b/net/core/dev.c
@@ -4169,8 +4169,7 @@ static struct sk_buff *sk_validate_xmit_skb(struct sk_buff *skb,
 static struct sk_buff *validate_xmit_unreadable_skb(struct sk_buff *skb,
 						    struct net_device *dev)
 {
-	struct skb_shared_info *shinfo;
-	struct net_iov *niov;
+	struct net_devmem_dmabuf_binding *binding;
 
 	if (likely(skb_frags_readable(skb) ||
 		   dev->netmem_tx == NETMEM_TX_NO_DMA))
@@ -4179,14 +4178,9 @@ static struct sk_buff *validate_xmit_unreadable_skb(struct sk_buff *skb,
 	if (dev->netmem_tx == NETMEM_TX_NONE)
 		goto out_free;
 
-	shinfo = skb_shinfo(skb);
-
-	if (shinfo->nr_frags > 0) {
-		niov = netmem_to_net_iov(skb_frag_netmem(&shinfo->frags[0]));
-		if (net_is_devmem_iov(niov) &&
-		    READ_ONCE(net_devmem_iov_binding(niov)->dev) != dev)
-			goto out_free;
-	}
+	binding = net_devmem_skb_binding(skb);
+	if (binding && READ_ONCE(binding->dev) != dev)
+		goto out_free;
 
 out:
 	return skb;
diff --git a/net/core/devmem.h b/net/core/devmem.h
index 942b8caf36e99..90058d7350820 100644
--- a/net/core/devmem.h
+++ b/net/core/devmem.h
@@ -10,6 +10,7 @@
 #ifndef _NET_DEVMEM_H
 #define _NET_DEVMEM_H
 
+#include <linux/skbuff.h>
 #include <net/net_namespace.h>
 #include <net/netmem.h>
 #include <net/netdev_netlink.h>
@@ -105,6 +106,21 @@ net_devmem_iov_binding(const struct net_iov *niov)
 	return niov->mp_priv;
 }
 
+static inline struct net_devmem_dmabuf_binding *
+net_devmem_skb_binding(const struct sk_buff *skb)
+{
+	struct net_iov *niov;
+
+	if (!skb_has_net_iovs(skb) || !skb_shinfo(skb)->nr_frags)
+		return NULL;
+
+	niov = netmem_to_net_iov(skb_frag_netmem(&skb_shinfo(skb)->frags[0]));
+	if (!net_is_devmem_iov(niov))
+		return NULL;
+
+	return net_devmem_iov_binding(niov);
+}
+
 static inline u32 net_devmem_iov_binding_id(const struct net_iov *niov)
 {
 	return net_devmem_iov_binding(niov)->id;
@@ -222,6 +238,12 @@ net_devmem_iov_binding(const struct net_iov *niov)
 {
 	return NULL;
 }
+
+static inline struct net_devmem_dmabuf_binding *
+net_devmem_skb_binding(const struct sk_buff *skb)
+{
+	return NULL;
+}
 #endif
 
 #endif /* _NET_DEVMEM_H */
diff --git a/net/core/gro.c b/net/core/gro.c
index 1b033855a20db..62b8d532218e5 100644
--- a/net/core/gro.c
+++ b/net/core/gro.c
@@ -100,16 +100,7 @@ int skb_gro_receive(struct sk_buff *p, struct sk_buff *skb)
 	struct sk_buff *lp;
 	int segs;
 
-	/* Do not splice page pool based packets w/ non-page pool
-	 * packets. This can result in reference count issues as page
-	 * pool pages will not decrement the reference count and will
-	 * instead be immediately returned to the pool or have frag
-	 * count decremented.
-	 */
-	if (p->pp_recycle != skb->pp_recycle)
-		return -ETOOMANYREFS;
-
-	if (skb_zcopy(p) || skb_zcopy(skb))
+	if (!skb_frags_can_coalesce(p, skb))
 		return -ETOOMANYREFS;
 
 	if (unlikely(p->len + len >= netif_get_gro_max_size(p->dev, p) ||
@@ -158,6 +149,8 @@ int skb_gro_receive(struct sk_buff *p, struct sk_buff *skb)
 
 		NAPI_GRO_CB(skb)->free = NAPI_GRO_FREE;
 		goto done;
+	} else if (!skb_frags_readable(skb)) {
+		return -ETOOMANYREFS;
 	} else if (skb->head_frag) {
 		int nr_frags = pinfo->nr_frags;
 		skb_frag_t *frag = pinfo->frags + nr_frags;
@@ -233,6 +226,9 @@ int skb_gro_receive(struct sk_buff *p, struct sk_buff *skb)
 
 int skb_gro_receive_list(struct sk_buff *p, struct sk_buff *skb)
 {
+	if (!skb_frags_can_coalesce(p, skb))
+		return -ETOOMANYREFS;
+
 	/* make sure to check flush flag and to not merge */
 	if (unlikely(p->len + skb->len >= 65536 ||
 		     NAPI_GRO_CB(skb)->flush))
diff --git a/net/core/net_test.c b/net/core/net_test.c
index 63124dbf3c9a8..99b664fc9186a 100644
--- a/net/core/net_test.c
+++ b/net/core/net_test.c
@@ -1485,7 +1485,7 @@ static void netmem_ref_test_try_coalesce(struct kunit *test)
 	};
 	struct sk_buff *to, *from, *clone;
 	struct net_devmem_dmabuf_binding binding = {};
-	struct net_iov niov = {};
+	struct net_iov niov = {}, niov2 = {};
 	struct page *pp_page, *reg_page;
 	netmem_ref pp_nm, niov_nm;
 	struct page_pool *pool;
@@ -1578,9 +1578,7 @@ static void netmem_ref_test_try_coalesce(struct kunit *test)
 	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);
@@ -1589,6 +1587,7 @@ static void netmem_ref_test_try_coalesce(struct kunit *test)
 	KUNIT_ASSERT_NOT_NULL(test, clone);
 	KUNIT_EXPECT_TRUE(test, skb_try_coalesce(to, from, &fragstolen,
 						 &delta));
+	KUNIT_EXPECT_TRUE(test, to->unreadable);
 	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);
@@ -1597,6 +1596,25 @@ static void netmem_ref_test_try_coalesce(struct kunit *test)
 	consume_skb(to);
 	KUNIT_EXPECT_EQ(test, atomic_long_read(&niov.desc.pp_ref_count), 2L);
 
+	/* 4. Reject coalescing net_iov skbs from different bindings. */
+	net_iov_init(&niov2, &binding.area, NET_IOV_DMABUF, &niov2);
+	to = alloc_skb(0, GFP_KERNEL);
+	from = alloc_skb(0, GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, to);
+	KUNIT_ASSERT_NOT_NULL(test, from);
+	to->pp_recycle = 1;
+	from->pp_recycle = 1;
+	skb_add_rx_frag_netmem(to, 0, niov_nm, 0, 32, PAGE_SIZE);
+	napi_pp_get_page(niov_nm);
+	skb_add_rx_frag_netmem(from, 0, net_iov_to_netmem(&niov2), 0, 32,
+			       PAGE_SIZE);
+	KUNIT_EXPECT_FALSE(test, skb_try_coalesce(to, from, &fragstolen,
+						  &delta));
+	skb_shinfo(from)->nr_frags = 0;
+	consume_skb(from);
+	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);
diff --git a/net/core/skbuff.c b/net/core/skbuff.c
index dc761ceb4d204..c588ae785f337 100644
--- a/net/core/skbuff.c
+++ b/net/core/skbuff.c
@@ -4342,10 +4342,9 @@ int skb_shift(struct sk_buff *tgt, struct sk_buff *skb, int shiftlen)
 
 	if (skb_headlen(skb))
 		return 0;
-	if (skb_zcopy(tgt) || skb_zcopy(skb))
+	if (!skb_frags_can_coalesce(tgt, skb))
 		return 0;
 
-	DEBUG_NET_WARN_ON_ONCE(tgt->pp_recycle != skb->pp_recycle);
 	DEBUG_NET_WARN_ON_ONCE(skb_cmp_decrypted(tgt, skb));
 
 	todo = shiftlen;
@@ -6301,17 +6300,8 @@ bool skb_try_coalesce(struct sk_buff *to, struct sk_buff *from,
 	 * tricky (due to potential race with the clone disappearing) and
 	 * rare, so not worth dealing with.
 	 */
-	if (to->pp_recycle != from->pp_recycle)
-		return false;
-
-	/* All frags in an skb must either all be struct page-backed or all
-	 * belong to the same memory provider instance; do not coalesce skbs
-	 * that would mix them.
-	 */
-	if (skb_frags_readable(from) != skb_frags_readable(to))
-		return false;
-
-	if (len <= skb_tailroom(to) && skb_frags_readable(from)) {
+	if (len <= skb_tailroom(to) && to->pp_recycle == from->pp_recycle &&
+	    skb_frags_readable(to) && skb_frags_readable(from)) {
 		if (len) {
 			if (unlikely(skb_copy_bits(from, 0,
 						   skb_tail_pointer(to),
@@ -6325,17 +6315,21 @@ bool skb_try_coalesce(struct sk_buff *to, struct sk_buff *from,
 		return true;
 	}
 
+	if (!skb_frags_can_coalesce(to, from))
+		return false;
+
 	to_shinfo = skb_shinfo(to);
 	from_shinfo = skb_shinfo(from);
 	if (to_shinfo->frag_list || from_shinfo->frag_list)
 		return false;
-	if (skb_zcopy(to) || skb_zcopy(from))
-		return false;
 
 	if (skb_headlen(from) != 0) {
 		struct page *page;
 		unsigned int offset;
 
+		if (!skb_frags_readable(from))
+			return false;
+
 		if (to_shinfo->nr_frags +
 		    from_shinfo->nr_frags >= MAX_SKB_FRAGS)
 			return false;
@@ -6367,6 +6361,7 @@ bool skb_try_coalesce(struct sk_buff *to, struct sk_buff *from,
 	to_shinfo->nr_frags += from_shinfo->nr_frags;
 	if (from_shinfo->nr_frags)
 		to_shinfo->flags |= from_shinfo->flags & SKBFL_SHARED_FRAG;
+	to->unreadable |= from->unreadable;
 
 	if (!skb_cloned(from))
 		from_shinfo->nr_frags = 0;
diff --git a/net/ipv4/inet_fragment.c b/net/ipv4/inet_fragment.c
index c18da91c1f031..a018ce6cf34ff 100644
--- a/net/ipv4/inet_fragment.c
+++ b/net/ipv4/inet_fragment.c
@@ -451,6 +451,9 @@ int inet_frag_queue_insert(struct inet_frag_queue *q, struct sk_buff *skb,
 	 * that reach the reassembled skb, whose head keeps the first
 	 * fragment's shinfo and whose frag_list is not GRO-shaped.
 	 */
+	if (last && !skb_mem_provider_compat(last, skb))
+		return IPFRAG_OVERLAP;
+
 	skb_gso_reset(skb);
 
 	/* RFC5722, Section 4, amended by Errata ID : 3089
diff --git a/net/ipv4/tcp.c b/net/ipv4/tcp.c
index b9de7c302ba83..67651b1936a8d 100644
--- a/net/ipv4/tcp.c
+++ b/net/ipv4/tcp.c
@@ -1251,7 +1251,7 @@ int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size)
 		trace_tcp_sendmsg_locked(sk, msg, skb, size_goal);
 
 		if (copy <= 0 || !tcp_skb_can_collapse_to(skb) ||
-		    unlikely(skb_frags_readable(skb) != !binding)) {
+		    unlikely(net_devmem_skb_binding(skb) != binding)) {
 			bool first_skb;
 
 new_segment:
diff --git a/net/ipv4/tcp_output.c b/net/ipv4/tcp_output.c
index cf3ddc055d48d..110dd1c775eb5 100644
--- a/net/ipv4/tcp_output.c
+++ b/net/ipv4/tcp_output.c
@@ -2657,6 +2657,9 @@ static bool tcp_can_coalesce_send_queue_head(struct sock *sk, int len)
 
 	skb = tcp_send_head(sk);
 	tcp_for_write_queue_from_safe(skb, next, sk) {
+		if (!skb_frags_readable(skb))
+			return false;
+
 		if (len <= skb->len)
 			break;
 
-- 
2.56.0.385.gac671c819a-goog


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* Re: [PATCH net-next v2 2/4] netmem: store memory provider private pointer directly in struct net_iov
  2026-11-02 23:18 ` [PATCH net-next v2 2/4] netmem: store memory provider private pointer directly in struct net_iov Mina Almasry
@ 2026-11-03 22:42   ` Stanislav Fomichev
  0 siblings, 0 replies; 8+ messages in thread
From: Stanislav Fomichev @ 2026-11-03 22:42 UTC (permalink / raw)
  To: Mina Almasry
  Cc: netdev, linux-kernel, Jakub Kicinski, Willem de Bruijn,
	Eric Dumazet, Kaiyuan Zhang, io-uring, Stanislav Fomichev,
	Paolo Abeni, David S. Miller, Simon Horman, Pavel Begunkov,
	Jens Axboe, Bobby Eshleman, Daniel Borkmann, Neal Cardwell,
	Kuniyuki Iwashima, David Ahern, Ido Schimmel

On 11/02, Mina Almasry wrote:

[..]

> Resolving the memory provider instance from a struct net_iov currently
> chases niov->owner (struct net_iov_area) and then dereferences the
> provider-specific container struct (such as net_devmem_dmabuf_binding
> or io_zcrx_area->ifq).

Chasing niov->owner is true, but I don't see another dereference you're
talking about? net_devmem_iov_binding does container_of to get from
area to the binding (and the area is the first member of the binding,
so it is a already a nop). And IIRC, iou has a similar arrangement.
So I'd vote to keep things as is unless you explain what I'm missing.

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH net-next v2 1/4] net: fix skb->unreadable propagation in skb_split() and skb_shift()
  2026-11-02 23:18 ` [PATCH net-next v2 1/4] net: fix skb->unreadable propagation in skb_split() and skb_shift() Mina Almasry
@ 2026-11-03 22:42   ` Stanislav Fomichev
  0 siblings, 0 replies; 8+ messages in thread
From: Stanislav Fomichev @ 2026-11-03 22:42 UTC (permalink / raw)
  To: Mina Almasry
  Cc: netdev, linux-kernel, Jakub Kicinski, Willem de Bruijn,
	Eric Dumazet, Kaiyuan Zhang, io-uring, Stanislav Fomichev,
	Paolo Abeni, David S. Miller, Simon Horman, Pavel Begunkov,
	Jens Axboe, Bobby Eshleman, Daniel Borkmann, Neal Cardwell,
	Kuniyuki Iwashima, David Ahern, Ido Schimmel

On 11/02, Mina Almasry wrote:
> skb_split() and skb_shift() do not accurately maintain skb->unreadable
> when moving fragments between skbs:
> 
> - skb_split_inside_header() moves all fragments out of skb into skb1
>   (leaving skb with nr_frags == 0 and only host-readable linear memory),
>   but leaves skb->unreadable set on skb.
> - skb_split_no_header() unconditionally copies skb->unreadable to skb1
>   even if 0 fragments were moved to skb1 (k == 0), and leaves
>   skb->unreadable set on skb when splitting at skb_headlen(skb) moves
>   all fragments into skb1 (!skb_shinfo(skb)->nr_frags).
> - skb_shift() neither propagates skb->unreadable to target skb when
>   shifting unreadable fragments into an empty skb, nor clears
>   skb->unreadable after all fragments are shifted out of skb.
> 
> Fix all three cases so skb->unreadable accurately tracks whether the
> remaining fragments in each skb are unreadable.
> 
> This issue is theoretical and was found by manual code inspection and
> LLM review; it was not triggered in production. Verified with the
> net_core KUnit test suite; not tested on real hardware.
> 
> Fixes: 65249feb6b3d ("net: add support for skbs with unreadable frags")
> Signed-off-by: Mina Almasry <almasrymina@google.com>

Acked-by: Stanislav Fomichev <sdf@fomichev.me>

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH net-next v2 3/4] net: add skb_frags_can_coalesce() helper
  2026-11-02 23:18 ` [PATCH net-next v2 3/4] net: add skb_frags_can_coalesce() helper Mina Almasry
@ 2026-11-03 22:43   ` Stanislav Fomichev
  0 siblings, 0 replies; 8+ messages in thread
From: Stanislav Fomichev @ 2026-11-03 22:43 UTC (permalink / raw)
  To: Mina Almasry
  Cc: netdev, linux-kernel, Jakub Kicinski, Willem de Bruijn,
	Eric Dumazet, Kaiyuan Zhang, io-uring, Stanislav Fomichev,
	Paolo Abeni, David S. Miller, Simon Horman, Pavel Begunkov,
	Jens Axboe, Bobby Eshleman, Daniel Borkmann, Neal Cardwell,
	Kuniyuki Iwashima, David Ahern, Ido Schimmel

On 11/02, Mina Almasry wrote:
> Add skb_has_net_iovs(skb), assuming skb->unreadable and net_iov-backed
> frags are equivalent for now (with a comment noting the two can be
> decoupled in the future if readable net_iovs are introduced).
> 
> Also add skb_mem_provider_compat() and skb_frags_can_coalesce() so all
> multi-skb coalescing, shifting, and GRO paths can share a single check
> across pp_recycle, skb_zcopy(), and fragment memory providers:
> - empty 0-length skbs are compatible without mixing readable payload
> - page vs net_iov is rejected via skb_has_net_iovs() (0 pointer chases)
> - page vs page is allowed via !skb_has_net_iovs() (0 pointer chases)
> - net_iov vs net_iov compares mp_priv (walking frag_list if nr_frags is 0)

Maybe squash this and patch #4? Otherwise hard to follow :-(

^ permalink raw reply	[flat|nested] 8+ messages in thread

end of thread, other threads:[~2026-11-03 22:43 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-02 23:18 [PATCH net-next v2 0/4] net: prevent mixing incompatible memory providers in skbs Mina Almasry
2026-11-02 23:18 ` [PATCH net-next v2 1/4] net: fix skb->unreadable propagation in skb_split() and skb_shift() Mina Almasry
2026-11-03 22:42   ` Stanislav Fomichev
2026-11-02 23:18 ` [PATCH net-next v2 2/4] netmem: store memory provider private pointer directly in struct net_iov Mina Almasry
2026-11-03 22:42   ` Stanislav Fomichev
2026-11-02 23:18 ` [PATCH net-next v2 3/4] net: add skb_frags_can_coalesce() helper Mina Almasry
2026-11-03 22:43   ` Stanislav Fomichev
2026-11-02 23:18 ` [PATCH net-next v2 4/4] net: prevent mixing frags from different memory providers or bindings Mina Almasry

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.