* [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; 5+ 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] 5+ 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-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, 0 replies; 5+ 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 [flat|nested] 5+ 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-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, 0 replies; 5+ 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 [flat|nested] 5+ 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-02 23:18 ` [PATCH net-next v2 4/4] net: prevent mixing frags from different memory providers or bindings Mina Almasry
3 siblings, 0 replies; 5+ 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 [flat|nested] 5+ 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; 5+ 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 [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-11-02 23:18 UTC | newest]
Thread overview: 5+ 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-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 ` [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
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®