Netdev List
 help / color / mirror / Atom feed
* [PATCH net-next v2 0/8] Add HW GRO handling in rmnet
@ 2026-10-08  0:55 Subash Abhinov Kasiviswanathan
  2026-10-08  0:55 ` [PATCH net-next v2 1/8] net: qualcomm: rmnet: Update MTU handling during format changes Subash Abhinov Kasiviswanathan
                   ` (7 more replies)
  0 siblings, 8 replies; 10+ messages in thread
From: Subash Abhinov Kasiviswanathan @ 2026-10-08  0:55 UTC (permalink / raw)
  To: davem, edumazet, kuba, pabeni, andrew+netdev, corbet
  Cc: horms, skhan, rdunlap, netdev, linux-doc, linux-kernel,
	Subash Abhinov Kasiviswanathan

This patch series adds downlink packet coalescing support to the rmnet driver.

Hardware can coalesce TCP and UDP packets into a single MAPv5 frame.
A frame can contain up to 48 packets grouped into up to six NLOs.
Packets within one NLO have the same length, while different NLOs may have
different lengths. Since an skb supports only one GSO size, the driver
reconstructs separate skbs for each NLO or packet run.

The coalescing header is a fixed 28-byte structure containing all six NLO
slots. The checksum error bitmap is a single 48-bit, frame-wide stream
consumed least significant bit first. This allows the driver to identify
checksum errors across NLO boundaries and for NLOs containing more than
eight packets.

The driver drops coalesced segments reported with bad transport checksums.
IPv4 UDP packets with a zero checksum remain valid because that checksum is
optional. Multi-packet coalesced frames require RX checksum offload and
hardware GRO support. Single-packet frames are delivered as normal non-GSO
skbs when those features are disabled.

v1->v2:
- All changes made are based on reports from Sashiko.
- Added a new patch "net: qualcomm: rmnet: Update MTU handling during format
  changes" to address the comment about the pre-existing issue about the MTU
  handling with data format updates.
- Document the restrictions on invalid combinations of data format in patch
  "net: qualcomm: rmnet: Restrict supported MAP checksum configurations" in
  comments and commit text.
- In patch "net: qualcomm: rmnet: Add DL packet coalescing support", add
  checks for packet content validation, better handling of the GRO_HW and
  RXCSUM flags, unclone prior to modifications, tagging drops with kfree_skb
  instead of consume_skb and updated comments about the coalescing packet
  format and updated commit text. 
- In patch "net: qualcomm: rmnet: Work around coalescing hardware quirks",
  updated the nlo field parsing checks and updated comments and commit text
  about the checksum related handling.
- In patch "net: qualcomm: rmnet: Add DL coalescing statistics", updated the
  stats to use per cpu counters to avoid torn reads, added network stack
  specific stats for HW_GRO and updated comments and commit text.
- In patch "docs: networking: Add documentation for the coalescing support in
  rmnet", updated the documentation and commit text to clarify the hardware behavior.

v1:
https://lore.kernel.org/all/20260930051345.857443-1-subash.a.kasiviswanathan@oss.qualcomm.com/

Assisted-by: LLM

Subash Abhinov Kasiviswanathan (8):
  net: qualcomm: rmnet: Update MTU handling during format changes
  uapi: if_link: Add RMNET_FLAGS_INGRESS_COALESCE
  net: qualcomm: rmnet: Process MAPv5 frames as a list
  net: qualcomm: rmnet: Restrict supported MAP checksum configurations
  net: qualcomm: rmnet: Add DL packet coalescing support
  net: qualcomm: rmnet: Work around coalescing hardware quirks
  net: qualcomm: rmnet: Add DL coalescing statistics
  docs: networking: Add documentation for the coalescing support in
    rmnet

 .../cellular/qualcomm/rmnet.rst               | 153 ++-
 .../ethernet/qualcomm/rmnet/rmnet_config.c    |  92 +-
 .../ethernet/qualcomm/rmnet/rmnet_config.h    |  84 +-
 .../ethernet/qualcomm/rmnet/rmnet_handlers.c  |  25 +-
 .../net/ethernet/qualcomm/rmnet/rmnet_map.h   |  20 +-
 .../ethernet/qualcomm/rmnet/rmnet_map_data.c  | 983 +++++++++++++++++-
 .../net/ethernet/qualcomm/rmnet/rmnet_vnd.c   | 193 +++-
 .../net/ethernet/qualcomm/rmnet/rmnet_vnd.h   |   4 +-
 include/linux/if_rmnet.h                      |  31 +
 include/uapi/linux/if_link.h                  |   1 +
 10 files changed, 1469 insertions(+), 117 deletions(-)

-- 
2.34.1


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

* [PATCH net-next v2 1/8] net: qualcomm: rmnet: Update MTU handling during format changes
  2026-10-08  0:55 [PATCH net-next v2 0/8] Add HW GRO handling in rmnet Subash Abhinov Kasiviswanathan
@ 2026-10-08  0:55 ` Subash Abhinov Kasiviswanathan
  2026-10-08  0:55 ` [PATCH net-next v2 2/8] uapi: if_link: Add RMNET_FLAGS_INGRESS_COALESCE Subash Abhinov Kasiviswanathan
                   ` (6 subsequent siblings)
  7 siblings, 0 replies; 10+ messages in thread
From: Subash Abhinov Kasiviswanathan @ 2026-10-08  0:55 UTC (permalink / raw)
  To: davem, edumazet, kuba, pabeni, andrew+netdev, corbet
  Cc: horms, skhan, rdunlap, netdev, linux-doc, linux-kernel,
	Subash Abhinov Kasiviswanathan, Sean Tranchetti

Calculate the MTU headroom from the requested data format when creating
an rmnet device. Resize existing endpoint MTUs when a newlink or changelink
requests a format that requires a smaller MTU.

Pass the requested format through the MTU helpers so the first device and
existing devices use the same headroom calculation. Validate the target MTU
before resizing existing endpoints and publish the new shared data format
only after all MTUs have been updated.

Co-developed-by: Sean Tranchetti <sean.tranchetti@oss.qualcomm.com>
Signed-off-by: Sean Tranchetti <sean.tranchetti@oss.qualcomm.com>
Signed-off-by: Subash Abhinov Kasiviswanathan <subash.a.kasiviswanathan@oss.qualcomm.com>
---
v2:
  - newly added patch in v2
  - fixes the pre-existing issue about MTU handling with data format
    updates as reported by Sashiko

 .../ethernet/qualcomm/rmnet/rmnet_config.c    | 55 +++++++++------
 .../net/ethernet/qualcomm/rmnet/rmnet_vnd.c   | 68 +++++++++++++------
 .../net/ethernet/qualcomm/rmnet/rmnet_vnd.h   |  4 +-
 3 files changed, 81 insertions(+), 46 deletions(-)

diff --git a/drivers/net/ethernet/qualcomm/rmnet/rmnet_config.c b/drivers/net/ethernet/qualcomm/rmnet/rmnet_config.c
index 61b04c6c0390..e5a6289b018a 100644
--- a/drivers/net/ethernet/qualcomm/rmnet/rmnet_config.c
+++ b/drivers/net/ethernet/qualcomm/rmnet/rmnet_config.c
@@ -143,6 +143,14 @@ static int rmnet_newlink(struct net_device *dev,
 		return -ENODEV;
 	}
 
+	if (data[IFLA_RMNET_FLAGS]) {
+		struct ifla_rmnet_flags *flags;
+
+		flags = nla_data(data[IFLA_RMNET_FLAGS]);
+		data_format &= ~flags->mask;
+		data_format |= flags->flags & flags->mask;
+	}
+
 	ep = kzalloc_obj(*ep);
 	if (!ep)
 		return -ENOMEM;
@@ -154,7 +162,8 @@ static int rmnet_newlink(struct net_device *dev,
 		goto err0;
 
 	port = rmnet_get_port_rtnl(real_dev);
-	err = rmnet_vnd_newlink(mux_id, dev, port, real_dev, ep, extack);
+	err = rmnet_vnd_newlink(mux_id, dev, port, real_dev, ep, extack,
+				data_format);
 	if (err)
 		goto err1;
 
@@ -162,24 +171,23 @@ static int rmnet_newlink(struct net_device *dev,
 	if (err < 0)
 		goto err2;
 
+	/* Update existing MTUs before publishing the shared data format. */
+	err = rmnet_vnd_update_dev_mtu(port, real_dev, data_format);
+	if (err)
+		goto err3;
+
 	port->rmnet_mode = mode;
 	port->rmnet_dev = dev;
 
 	hlist_add_head_rcu(&ep->hlnode, &port->muxed_ep[mux_id]);
 
-	if (data[IFLA_RMNET_FLAGS]) {
-		struct ifla_rmnet_flags *flags;
-
-		flags = nla_data(data[IFLA_RMNET_FLAGS]);
-		data_format &= ~flags->mask;
-		data_format |= flags->flags & flags->mask;
-	}
-
 	netdev_dbg(dev, "data format [0x%08X]\n", data_format);
 	WRITE_ONCE(port->data_format, data_format);
 
 	return 0;
 
+err3:
+	netdev_upper_dev_unlink(real_dev, dev);
 err2:
 	unregister_netdevice(dev);
 	rmnet_vnd_dellink(mux_id, port, ep);
@@ -301,9 +309,13 @@ static int rmnet_changelink(struct net_device *dev, struct nlattr *tb[],
 			    struct netlink_ext_ack *extack)
 {
 	struct rmnet_priv *priv = netdev_priv(dev);
+	struct ifla_rmnet_flags *flags;
 	struct net_device *real_dev;
 	struct rmnet_port *port;
+	u32 old_data_format;
+	u32 data_format;
 	u16 mux_id;
+	int err;
 
 	if (!dev)
 		return -ENODEV;
@@ -320,6 +332,13 @@ static int rmnet_changelink(struct net_device *dev, struct nlattr *tb[],
 
 	port = rmnet_get_port_rtnl(real_dev);
 
+	if (data[IFLA_RMNET_FLAGS]) {
+		old_data_format = READ_ONCE(port->data_format);
+		flags = nla_data(data[IFLA_RMNET_FLAGS]);
+		data_format = old_data_format & ~flags->mask;
+		data_format |= flags->flags & flags->mask;
+	}
+
 	if (data[IFLA_RMNET_MUX_ID]) {
 		mux_id = nla_get_u16(data[IFLA_RMNET_MUX_ID]);
 
@@ -346,21 +365,13 @@ static int rmnet_changelink(struct net_device *dev, struct nlattr *tb[],
 	}
 
 	if (data[IFLA_RMNET_FLAGS]) {
-		struct ifla_rmnet_flags *flags;
-		u32 old_data_format;
-		u32 data_format;
-
-		old_data_format = port->data_format;
-		flags = nla_data(data[IFLA_RMNET_FLAGS]);
-		data_format = old_data_format & ~flags->mask;
-		data_format |= flags->flags & flags->mask;
-		WRITE_ONCE(port->data_format, data_format);
-
-		if (rmnet_vnd_update_dev_mtu(port, real_dev)) {
-			WRITE_ONCE(port->data_format, old_data_format);
+		err = rmnet_vnd_update_dev_mtu(port, real_dev, data_format);
+		if (err) {
 			NL_SET_ERR_MSG_MOD(extack, "Invalid MTU on real dev");
-			return -EINVAL;
+			return err;
 		}
+
+		WRITE_ONCE(port->data_format, data_format);
 	}
 
 	return 0;
diff --git a/drivers/net/ethernet/qualcomm/rmnet/rmnet_vnd.c b/drivers/net/ethernet/qualcomm/rmnet/rmnet_vnd.c
index 5f921cddf82b..d23f74b0aa47 100644
--- a/drivers/net/ethernet/qualcomm/rmnet/rmnet_vnd.c
+++ b/drivers/net/ethernet/qualcomm/rmnet/rmnet_vnd.c
@@ -66,27 +66,25 @@ static netdev_tx_t rmnet_vnd_start_xmit(struct sk_buff *skb,
 	return NETDEV_TX_OK;
 }
 
-static int rmnet_vnd_headroom(struct rmnet_port *port)
+static int rmnet_vnd_headroom(u32 data_format)
 {
 	u32 headroom;
 
 	headroom = sizeof(struct rmnet_map_header);
 
-	if (port->data_format & RMNET_FLAGS_EGRESS_MAP_CKSUMV4)
+	if (data_format & RMNET_FLAGS_EGRESS_MAP_CKSUMV4)
 		headroom += sizeof(struct rmnet_map_ul_csum_header);
 
 	return headroom;
 }
 
-static int rmnet_vnd_change_mtu(struct net_device *rmnet_dev, int new_mtu)
+static int rmnet_vnd_change_mtu_with_format(struct net_device *rmnet_dev,
+					    int new_mtu, u32 data_format)
 {
 	struct rmnet_priv *priv = netdev_priv(rmnet_dev);
-	struct rmnet_port *port;
 	u32 headroom;
 
-	port = rmnet_get_port_rtnl(priv->real_dev);
-
-	headroom = rmnet_vnd_headroom(port);
+	headroom = rmnet_vnd_headroom(data_format);
 
 	if (new_mtu < 0 || new_mtu > RMNET_MAX_PACKET_SIZE ||
 	    new_mtu > (priv->real_dev->mtu - headroom))
@@ -96,6 +94,17 @@ static int rmnet_vnd_change_mtu(struct net_device *rmnet_dev, int new_mtu)
 	return 0;
 }
 
+static int rmnet_vnd_change_mtu(struct net_device *rmnet_dev, int new_mtu)
+{
+	struct rmnet_priv *priv = netdev_priv(rmnet_dev);
+	struct rmnet_port *port;
+
+	port = rmnet_get_port_rtnl(priv->real_dev);
+
+	return rmnet_vnd_change_mtu_with_format(rmnet_dev, new_mtu,
+						READ_ONCE(port->data_format));
+}
+
 static int rmnet_vnd_get_iflink(const struct net_device *dev)
 {
 	struct rmnet_priv *priv = netdev_priv(dev);
@@ -308,8 +317,7 @@ int rmnet_vnd_newlink(u8 id, struct net_device *rmnet_dev,
 		      struct rmnet_port *port,
 		      struct net_device *real_dev,
 		      struct rmnet_endpoint *ep,
-		      struct netlink_ext_ack *extack)
-
+		      struct netlink_ext_ack *extack, u32 data_format)
 {
 	struct rmnet_priv *priv = netdev_priv(rmnet_dev);
 	u32 headroom;
@@ -326,9 +334,11 @@ int rmnet_vnd_newlink(u8 id, struct net_device *rmnet_dev,
 
 	priv->real_dev = real_dev;
 
-	headroom = rmnet_vnd_headroom(port);
+	headroom = rmnet_vnd_headroom(data_format);
 
-	if (rmnet_vnd_change_mtu(rmnet_dev, real_dev->mtu - headroom)) {
+	if (rmnet_vnd_change_mtu_with_format(rmnet_dev,
+					     real_dev->mtu - headroom,
+					     data_format)) {
 		NL_SET_ERR_MSG_MOD(extack, "Invalid MTU on real dev");
 		return -EINVAL;
 	}
@@ -375,17 +385,15 @@ int rmnet_vnd_do_flow_control(struct net_device *rmnet_dev, int enable)
 	return 0;
 }
 
-int rmnet_vnd_validate_real_dev_mtu(struct net_device *real_dev)
+static int rmnet_vnd_validate_mtu(struct rmnet_port *port,
+				  struct net_device *real_dev, u32 data_format)
 {
 	struct hlist_node *tmp_ep;
 	struct rmnet_endpoint *ep;
-	struct rmnet_port *port;
 	unsigned long bkt_ep;
 	u32 headroom;
 
-	port = rmnet_get_port_rtnl(real_dev);
-
-	headroom = rmnet_vnd_headroom(port);
+	headroom = rmnet_vnd_headroom(data_format);
 
 	hash_for_each_safe(port->muxed_ep, bkt_ep, tmp_ep, ep, hlnode) {
 		if (ep->egress_dev->mtu > (real_dev->mtu - headroom))
@@ -395,23 +403,39 @@ int rmnet_vnd_validate_real_dev_mtu(struct net_device *real_dev)
 	return 0;
 }
 
+int rmnet_vnd_validate_real_dev_mtu(struct net_device *real_dev)
+{
+	struct rmnet_port *port;
+
+	port = rmnet_get_port_rtnl(real_dev);
+
+	return rmnet_vnd_validate_mtu(port, real_dev,
+				      READ_ONCE(port->data_format));
+}
+
 int rmnet_vnd_update_dev_mtu(struct rmnet_port *port,
-			     struct net_device *real_dev)
+			     struct net_device *real_dev, u32 data_format)
 {
 	struct hlist_node *tmp_ep;
 	struct rmnet_endpoint *ep;
 	unsigned long bkt_ep;
 	u32 headroom;
+	int new_mtu;
 
-	headroom = rmnet_vnd_headroom(port);
+	headroom = rmnet_vnd_headroom(data_format);
+	if (real_dev->mtu < headroom)
+		return -EINVAL;
+
+	new_mtu = real_dev->mtu - headroom;
+	if (new_mtu > RMNET_MAX_PACKET_SIZE)
+		return -EINVAL;
 
 	hash_for_each_safe(port->muxed_ep, bkt_ep, tmp_ep, ep, hlnode) {
-		if (ep->egress_dev->mtu <= (real_dev->mtu - headroom))
+		if (ep->egress_dev->mtu <= new_mtu)
 			continue;
 
-		if (rmnet_vnd_change_mtu(ep->egress_dev,
-					 real_dev->mtu - headroom))
-			return -1;
+		/* The target MTU was validated before updating any endpoint. */
+		WRITE_ONCE(ep->egress_dev->mtu, new_mtu);
 	}
 
 	return 0;
diff --git a/drivers/net/ethernet/qualcomm/rmnet/rmnet_vnd.h b/drivers/net/ethernet/qualcomm/rmnet/rmnet_vnd.h
index c2b2baf86894..901ef36900ac 100644
--- a/drivers/net/ethernet/qualcomm/rmnet/rmnet_vnd.h
+++ b/drivers/net/ethernet/qualcomm/rmnet/rmnet_vnd.h
@@ -12,7 +12,7 @@ int rmnet_vnd_newlink(u8 id, struct net_device *rmnet_dev,
 		      struct rmnet_port *port,
 		      struct net_device *real_dev,
 		      struct rmnet_endpoint *ep,
-		      struct netlink_ext_ack *extack);
+		      struct netlink_ext_ack *extack, u32 data_format);
 int rmnet_vnd_dellink(u8 id, struct rmnet_port *port,
 		      struct rmnet_endpoint *ep);
 void rmnet_vnd_rx_fixup(struct sk_buff *skb, struct net_device *dev);
@@ -21,5 +21,5 @@ void rmnet_vnd_tx_fixup(struct sk_buff *skb, struct net_device *dev);
 void rmnet_vnd_setup(struct net_device *dev);
 int rmnet_vnd_validate_real_dev_mtu(struct net_device *real_dev);
 int rmnet_vnd_update_dev_mtu(struct rmnet_port *port,
-			     struct net_device *real_dev);
+			     struct net_device *real_dev, u32 data_format);
 #endif /* _RMNET_VND_H_ */
-- 
2.34.1


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

* [PATCH net-next v2 2/8] uapi: if_link: Add RMNET_FLAGS_INGRESS_COALESCE
  2026-10-08  0:55 [PATCH net-next v2 0/8] Add HW GRO handling in rmnet Subash Abhinov Kasiviswanathan
  2026-10-08  0:55 ` [PATCH net-next v2 1/8] net: qualcomm: rmnet: Update MTU handling during format changes Subash Abhinov Kasiviswanathan
@ 2026-10-08  0:55 ` Subash Abhinov Kasiviswanathan
  2026-10-08  0:55 ` [PATCH net-next v2 3/8] net: qualcomm: rmnet: Process MAPv5 frames as a list Subash Abhinov Kasiviswanathan
                   ` (5 subsequent siblings)
  7 siblings, 0 replies; 10+ messages in thread
From: Subash Abhinov Kasiviswanathan @ 2026-10-08  0:55 UTC (permalink / raw)
  To: davem, edumazet, kuba, pabeni, andrew+netdev, corbet
  Cc: horms, skhan, rdunlap, netdev, linux-doc, linux-kernel,
	Subash Abhinov Kasiviswanathan, Sean Tranchetti

Add bit 6 of the rmnet ingress flags to allow rmnet to process incoming
packets in the MAPv5 coalescing header format.

Co-developed-by: Sean Tranchetti <sean.tranchetti@oss.qualcomm.com>
Signed-off-by: Sean Tranchetti <sean.tranchetti@oss.qualcomm.com>
Signed-off-by: Subash Abhinov Kasiviswanathan <subash.a.kasiviswanathan@oss.qualcomm.com>
---
v2: no change
v1: https://lore.kernel.org/all/20260930051345.857443-2-subash.a.kasiviswanathan@oss.qualcomm.com/

 include/uapi/linux/if_link.h | 1 +
 1 file changed, 1 insertion(+)

diff --git a/include/uapi/linux/if_link.h b/include/uapi/linux/if_link.h
index 245b36204525..349fcddc6622 100644
--- a/include/uapi/linux/if_link.h
+++ b/include/uapi/linux/if_link.h
@@ -2022,6 +2022,7 @@ enum {
 #define RMNET_FLAGS_EGRESS_MAP_CKSUMV4            (1U << 3)
 #define RMNET_FLAGS_INGRESS_MAP_CKSUMV5           (1U << 4)
 #define RMNET_FLAGS_EGRESS_MAP_CKSUMV5            (1U << 5)
+#define RMNET_FLAGS_INGRESS_COALESCE              (1U << 6)
 
 enum {
 	IFLA_RMNET_UNSPEC,
-- 
2.34.1


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

* [PATCH net-next v2 3/8] net: qualcomm: rmnet: Process MAPv5 frames as a list
  2026-10-08  0:55 [PATCH net-next v2 0/8] Add HW GRO handling in rmnet Subash Abhinov Kasiviswanathan
  2026-10-08  0:55 ` [PATCH net-next v2 1/8] net: qualcomm: rmnet: Update MTU handling during format changes Subash Abhinov Kasiviswanathan
  2026-10-08  0:55 ` [PATCH net-next v2 2/8] uapi: if_link: Add RMNET_FLAGS_INGRESS_COALESCE Subash Abhinov Kasiviswanathan
@ 2026-10-08  0:55 ` Subash Abhinov Kasiviswanathan
  2026-10-08  0:55 ` [PATCH net-next v2 4/8] net: qualcomm: rmnet: Restrict supported MAP checksum configurations Subash Abhinov Kasiviswanathan
                   ` (4 subsequent siblings)
  7 siblings, 0 replies; 10+ messages in thread
From: Subash Abhinov Kasiviswanathan @ 2026-10-08  0:55 UTC (permalink / raw)
  To: davem, edumazet, kuba, pabeni, andrew+netdev, corbet
  Cc: horms, skhan, rdunlap, netdev, linux-doc, linux-kernel,
	Subash Abhinov Kasiviswanathan, Sean Tranchetti

Add support for handling the MAP packets as a list in preparation for
the subsequent patch in the series  which adds support for handling the
coalescing packets. This is needed as a single coalescing MAP packet could
yield multiple IP packets.

There is no functional change in the handling of the previously
supported MAP packet formats.

Co-developed-by: Sean Tranchetti <sean.tranchetti@oss.qualcomm.com>
Signed-off-by: Sean Tranchetti <sean.tranchetti@oss.qualcomm.com>
Signed-off-by: Subash Abhinov Kasiviswanathan <subash.a.kasiviswanathan@oss.qualcomm.com>
---
v2: no change
v1: https://lore.kernel.org/all/20260930051345.857443-3-subash.a.kasiviswanathan@oss.qualcomm.com/

 .../ethernet/qualcomm/rmnet/rmnet_handlers.c  | 22 +++++--
 .../net/ethernet/qualcomm/rmnet/rmnet_map.h   |  4 +-
 .../ethernet/qualcomm/rmnet/rmnet_map_data.c  | 57 ++++++++++++-------
 3 files changed, 58 insertions(+), 25 deletions(-)

diff --git a/drivers/net/ethernet/qualcomm/rmnet/rmnet_handlers.c b/drivers/net/ethernet/qualcomm/rmnet/rmnet_handlers.c
index aa5523f4618e..ab1dfbd833e3 100644
--- a/drivers/net/ethernet/qualcomm/rmnet/rmnet_handlers.c
+++ b/drivers/net/ethernet/qualcomm/rmnet/rmnet_handlers.c
@@ -50,6 +50,16 @@ rmnet_deliver_skb(struct sk_buff *skb)
 	gro_cells_receive(&priv->gro_cells, skb);
 }
 
+static void rmnet_deliver_skb_list(struct sk_buff_head *head)
+{
+	struct sk_buff *skb;
+
+	while ((skb = __skb_dequeue(head))) {
+		rmnet_set_skb_proto(skb);
+		rmnet_deliver_skb(skb);
+	}
+}
+
 /* MAP handler */
 
 static void
@@ -59,6 +69,7 @@ __rmnet_map_ingress_handler(struct sk_buff *skb,
 {
 	struct rmnet_map_header *map_header = (void *)skb->data;
 	struct rmnet_endpoint *ep;
+	struct sk_buff_head list;
 	u16 len, pad;
 	u8 mux_id;
 
@@ -83,12 +94,12 @@ __rmnet_map_ingress_handler(struct sk_buff *skb,
 
 	skb->dev = ep->egress_dev;
 
+	__skb_queue_head_init(&list);
+
 	if ((data_format & RMNET_FLAGS_INGRESS_MAP_CKSUMV5) &&
 	    (map_header->flags & MAP_NEXT_HEADER_FLAG)) {
-		if (rmnet_map_process_next_hdr_packet(skb, len))
+		if (rmnet_map_process_next_hdr_packet(skb, &list, len))
 			goto free_skb;
-		skb_pull(skb, sizeof(*map_header));
-		rmnet_set_skb_proto(skb);
 	} else {
 		/* Subtract MAP header */
 		skb_pull(skb, sizeof(*map_header));
@@ -96,10 +107,11 @@ __rmnet_map_ingress_handler(struct sk_buff *skb,
 		if (data_format & RMNET_FLAGS_INGRESS_MAP_CKSUMV4 &&
 		    !rmnet_map_checksum_downlink_packet(skb, len + pad))
 			skb->ip_summed = CHECKSUM_UNNECESSARY;
+		skb_trim(skb, len);
+		__skb_queue_tail(&list, skb);
 	}
 
-	skb_trim(skb, len);
-	rmnet_deliver_skb(skb);
+	rmnet_deliver_skb_list(&list);
 	return;
 
 free_skb:
diff --git a/drivers/net/ethernet/qualcomm/rmnet/rmnet_map.h b/drivers/net/ethernet/qualcomm/rmnet/rmnet_map.h
index 0977e495f591..ef738ce015d4 100644
--- a/drivers/net/ethernet/qualcomm/rmnet/rmnet_map.h
+++ b/drivers/net/ethernet/qualcomm/rmnet/rmnet_map.h
@@ -53,7 +53,9 @@ void rmnet_map_checksum_uplink_packet(struct sk_buff *skb,
 				      struct rmnet_port *port,
 				      struct net_device *orig_dev,
 				      int csum_type);
-int rmnet_map_process_next_hdr_packet(struct sk_buff *skb, u16 len);
+int rmnet_map_process_next_hdr_packet(struct sk_buff *skb,
+				      struct sk_buff_head *list,
+				      u16 len);
 unsigned int rmnet_map_tx_aggregate(struct sk_buff *skb, struct rmnet_port *port,
 				    struct net_device *orig_dev);
 void rmnet_map_tx_aggregate_init(struct rmnet_port *port);
diff --git a/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c b/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c
index 39d6d084e73f..577f2758e385 100644
--- a/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c
+++ b/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c
@@ -497,35 +497,54 @@ void rmnet_map_checksum_uplink_packet(struct sk_buff *skb,
 	}
 }
 
-/* Process a MAPv5 packet header */
+static struct rmnet_map_v5_csum_header *
+rmnet_map_get_next_hdr(struct sk_buff *skb)
+{
+	return (struct rmnet_map_v5_csum_header *)(skb->data +
+						   sizeof(struct rmnet_map_header));
+}
+
+static u8 rmnet_map_get_next_hdr_type(struct sk_buff *skb)
+{
+	struct rmnet_map_v5_csum_header *hdr = rmnet_map_get_next_hdr(skb);
+
+	return u8_get_bits(hdr->header_info, MAPV5_HDRINFO_HDR_TYPE_FMASK);
+}
+
+static bool rmnet_map_get_csum_valid(struct sk_buff *skb)
+{
+	struct rmnet_map_v5_csum_header *hdr = rmnet_map_get_next_hdr(skb);
+
+	return !!(hdr->csum_info & MAPV5_CSUMINFO_VALID_FLAG);
+}
+
 int rmnet_map_process_next_hdr_packet(struct sk_buff *skb,
+				      struct sk_buff_head *list,
 				      u16 len)
 {
 	struct rmnet_priv *priv = netdev_priv(skb->dev);
-	struct rmnet_map_v5_csum_header *next_hdr;
-	u8 nexthdr_type;
 
-	next_hdr = (struct rmnet_map_v5_csum_header *)(skb->data +
-			sizeof(struct rmnet_map_header));
+	switch (rmnet_map_get_next_hdr_type(skb)) {
+	case RMNET_MAP_HEADER_TYPE_CSUM_OFFLOAD:
+		if (unlikely(!(skb->dev->features & NETIF_F_RXCSUM))) {
+			priv->stats.csum_sw++;
+		} else if (rmnet_map_get_csum_valid(skb)) {
+			priv->stats.csum_ok++;
+			skb->ip_summed = CHECKSUM_UNNECESSARY;
+		} else {
+			priv->stats.csum_valid_unset++;
+		}
 
-	nexthdr_type = u8_get_bits(next_hdr->header_info,
-				   MAPV5_HDRINFO_HDR_TYPE_FMASK);
+		skb_pull(skb, sizeof(struct rmnet_map_header) +
+			      sizeof(struct rmnet_map_v5_csum_header));
+		skb_trim(skb, len);
+		__skb_queue_tail(list, skb);
+		break;
 
-	if (nexthdr_type != RMNET_MAP_HEADER_TYPE_CSUM_OFFLOAD)
+	default:
 		return -EINVAL;
-
-	if (unlikely(!(skb->dev->features & NETIF_F_RXCSUM))) {
-		priv->stats.csum_sw++;
-	} else if (next_hdr->csum_info & MAPV5_CSUMINFO_VALID_FLAG) {
-		priv->stats.csum_ok++;
-		skb->ip_summed = CHECKSUM_UNNECESSARY;
-	} else {
-		priv->stats.csum_valid_unset++;
 	}
 
-	/* Pull csum v5 header */
-	skb_pull(skb, sizeof(*next_hdr));
-
 	return 0;
 }
 
-- 
2.34.1


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

* [PATCH net-next v2 4/8] net: qualcomm: rmnet: Restrict supported MAP checksum configurations
  2026-10-08  0:55 [PATCH net-next v2 0/8] Add HW GRO handling in rmnet Subash Abhinov Kasiviswanathan
                   ` (2 preceding siblings ...)
  2026-10-08  0:55 ` [PATCH net-next v2 3/8] net: qualcomm: rmnet: Process MAPv5 frames as a list Subash Abhinov Kasiviswanathan
@ 2026-10-08  0:55 ` Subash Abhinov Kasiviswanathan
  2026-10-08  0:55 ` [PATCH net-next v2 5/8] net: qualcomm: rmnet: Add DL packet coalescing support Subash Abhinov Kasiviswanathan
                   ` (3 subsequent siblings)
  7 siblings, 0 replies; 10+ messages in thread
From: Subash Abhinov Kasiviswanathan @ 2026-10-08  0:55 UTC (permalink / raw)
  To: davem, edumazet, kuba, pabeni, andrew+netdev, corbet
  Cc: horms, skhan, rdunlap, netdev, linux-doc, linux-kernel,
	Subash Abhinov Kasiviswanathan, Sean Tranchetti

RmNet only supports three MAP data format configurations: MAPv1 (no
checksum offload), MAPv4 (v4 checksum offload) and MAPv5 (v5 checksum
offload). MAP command support is orthogonal and may be combined with any
of the three. Checksum offload versions are selected independently for
ingress and egress, but only one version may be enabled across the device.
Mixing the v4 and v5 checksum offload flags together, whether on the same
direction or across directions, is not a valid configuration. A direction
may leave checksum offload disabled.

Validate the requested data format in both rmnet_newlink() and
rmnet_changelink() and reject any combination that sets both the v4 and
v5 checksum offload flags at the same time, regardless of direction. This
is in preparation for the next patch where coalescing support needs to be
allowed with MAPv5 format only.

Co-developed-by: Sean Tranchetti <sean.tranchetti@oss.qualcomm.com>
Signed-off-by: Sean Tranchetti <sean.tranchetti@oss.qualcomm.com>
Signed-off-by: Subash Abhinov Kasiviswanathan <subash.a.kasiviswanathan@oss.qualcomm.com>
---
v2: 
  - Document the restrictions on invalid combinations of data format as
    reported by Sashiko
v1: https://lore.kernel.org/all/20260930051345.857443-4-subash.a.kasiviswanathan@oss.qualcomm.com/

 .../ethernet/qualcomm/rmnet/rmnet_config.c    | 29 +++++++++++++++++++
 1 file changed, 29 insertions(+)

diff --git a/drivers/net/ethernet/qualcomm/rmnet/rmnet_config.c b/drivers/net/ethernet/qualcomm/rmnet/rmnet_config.c
index e5a6289b018a..b68f5561680f 100644
--- a/drivers/net/ethernet/qualcomm/rmnet/rmnet_config.c
+++ b/drivers/net/ethernet/qualcomm/rmnet/rmnet_config.c
@@ -26,6 +26,23 @@ static int rmnet_is_real_dev_registered(const struct net_device *real_dev)
 	return rcu_access_pointer(real_dev->rx_handler) == rmnet_rx_handler;
 }
 
+/* Only three MAP configurations are supported: MAPv1 (no checksum
+ * offload), MAPv4 (v4 checksum offload) and MAPv5 (v5 checksum
+ * offload). MAP command support is orthogonal and permitted with any
+ * of the three. Checksum offload versions are selected independently
+ * for ingress and egress, but only one version may be enabled across
+ * the device. Mixing v4 and v5 checksum flags is not supported.
+ */
+static bool rmnet_config_data_format_valid(u32 data_format)
+{
+	u32 v4_mask = RMNET_FLAGS_INGRESS_MAP_CKSUMV4 |
+		      RMNET_FLAGS_EGRESS_MAP_CKSUMV4;
+	u32 v5_mask = RMNET_FLAGS_INGRESS_MAP_CKSUMV5 |
+		      RMNET_FLAGS_EGRESS_MAP_CKSUMV5;
+
+	return !(data_format & v4_mask) || !(data_format & v5_mask);
+}
+
 /* Needs rtnl lock */
 struct rmnet_port*
 rmnet_get_port_rtnl(const struct net_device *real_dev)
@@ -151,6 +168,12 @@ static int rmnet_newlink(struct net_device *dev,
 		data_format |= flags->flags & flags->mask;
 	}
 
+	if (!rmnet_config_data_format_valid(data_format)) {
+		NL_SET_ERR_MSG_MOD(extack,
+				   "unsupported MAP checksum flag combination");
+		return -EINVAL;
+	}
+
 	ep = kzalloc_obj(*ep);
 	if (!ep)
 		return -ENOMEM;
@@ -337,6 +360,12 @@ static int rmnet_changelink(struct net_device *dev, struct nlattr *tb[],
 		flags = nla_data(data[IFLA_RMNET_FLAGS]);
 		data_format = old_data_format & ~flags->mask;
 		data_format |= flags->flags & flags->mask;
+
+		if (!rmnet_config_data_format_valid(data_format)) {
+			NL_SET_ERR_MSG_MOD(extack,
+					   "unsupported MAP checksum flag combination");
+			return -EINVAL;
+		}
 	}
 
 	if (data[IFLA_RMNET_MUX_ID]) {
-- 
2.34.1


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

* [PATCH net-next v2 5/8] net: qualcomm: rmnet: Add DL packet coalescing support
  2026-10-08  0:55 [PATCH net-next v2 0/8] Add HW GRO handling in rmnet Subash Abhinov Kasiviswanathan
                   ` (3 preceding siblings ...)
  2026-10-08  0:55 ` [PATCH net-next v2 4/8] net: qualcomm: rmnet: Restrict supported MAP checksum configurations Subash Abhinov Kasiviswanathan
@ 2026-10-08  0:55 ` Subash Abhinov Kasiviswanathan
  2026-10-08  0:55 ` [PATCH net-next v2 6/8] net: qualcomm: rmnet: Work around coalescing hardware quirks Subash Abhinov Kasiviswanathan
                   ` (2 subsequent siblings)
  7 siblings, 0 replies; 10+ messages in thread
From: Subash Abhinov Kasiviswanathan @ 2026-10-08  0:55 UTC (permalink / raw)
  To: davem, edumazet, kuba, pabeni, andrew+netdev, corbet
  Cc: horms, skhan, rdunlap, netdev, linux-doc, linux-kernel,
	Subash Abhinov Kasiviswanathan, Sean Tranchetti

Hardware can coalesce multiple IP packets of a flow into a single MAP
frame carrying a MAPv5 coalescing header and a set of Number Length
Objects (NLOs) that describe the count and length of the individual
segments within. The MAP coalescing header encapsulates a single IP
packet of type IPv4 and IPv6 for protocols TCP and UDP. Up to 48 IP
packets can be coalesced into a single coalescing packet.

This means that hardware can coalesce up to 6 different sizes within a
single IP packet. Additionally, hardware can continue to coalesce
packets even though it may have detected an issue with the checksum. The
checksum bitmap will give information on the exact segments which
happened to encounter a bad checksum. In these cases, the packets need
to be segmented out from the whole IP packet and only the packets with
same size need to be queued to the stack for valid checksum packets
while invalid checksum packets are dropped in the driver.

Coalesced frames containing multiple packets are accepted only when
both RXCSUM and GRO_HW are enabled. Such packets are either queued
through the GRO fast path or segmented in the driver. The resulting SKBs
carry CHECKSUM_PARTIAL and SKBs representing multiple packets also carry
the appropriate GSO metadata.

GRO_HW is advertised as an optional hardware feature and can be enabled
through the normal netdev feature controls. A single-packet frame is
passed with CHECKSUM_UNNECESSARY when both features are enabled and with
CHECKSUM_NONE when either feature is disabled. Multi-packet coalesced
frames are dropped when either RXCSUM or GRO_HW is disabled.

If a packet is segmented, the IP length and checksum are also updated
accordingly.

DL packet coalescing relies on the MAPv5 checksum header being present
on ingress, so reject any configuration which requests coalescing without
also enabling MAPv5 ingress checksum offload.

Co-developed-by: Sean Tranchetti <sean.tranchetti@oss.qualcomm.com>
Signed-off-by: Sean Tranchetti <sean.tranchetti@oss.qualcomm.com>
Signed-off-by: Subash Abhinov Kasiviswanathan <subash.a.kasiviswanathan@oss.qualcomm.com>
---
v2: 
  - Add checks for packet content validation to avoid invalid access
  - Add better handling of the GRO_HW and RXCSUM flags
  - unclone prior to coalescing packet modifications
  - Tag drops with kfree_skb instead of consume_skb
  - Add more comments and update commit text about the coalescing
    operations
v1: https://lore.kernel.org/all/20260930051345.857443-5-subash.a.kasiviswanathan@oss.qualcomm.com/

 .../ethernet/qualcomm/rmnet/rmnet_config.c    |  12 +-
 .../ethernet/qualcomm/rmnet/rmnet_handlers.c  |   5 +-
 .../net/ethernet/qualcomm/rmnet/rmnet_map.h   |  18 +-
 .../ethernet/qualcomm/rmnet/rmnet_map_data.c  | 560 +++++++++++++++++-
 .../net/ethernet/qualcomm/rmnet/rmnet_vnd.c   |   1 +
 include/linux/if_rmnet.h                      |  31 +
 6 files changed, 611 insertions(+), 16 deletions(-)

diff --git a/drivers/net/ethernet/qualcomm/rmnet/rmnet_config.c b/drivers/net/ethernet/qualcomm/rmnet/rmnet_config.c
index b68f5561680f..ec882ad371ab 100644
--- a/drivers/net/ethernet/qualcomm/rmnet/rmnet_config.c
+++ b/drivers/net/ethernet/qualcomm/rmnet/rmnet_config.c
@@ -31,7 +31,8 @@ static int rmnet_is_real_dev_registered(const struct net_device *real_dev)
  * offload). MAP command support is orthogonal and permitted with any
  * of the three. Checksum offload versions are selected independently
  * for ingress and egress, but only one version may be enabled across
- * the device. Mixing v4 and v5 checksum flags is not supported.
+ * the device. Mixing v4 and v5 checksum flags is not supported. DL
+ * packet coalescing additionally requires a MAPv5 configuration.
  */
 static bool rmnet_config_data_format_valid(u32 data_format)
 {
@@ -40,7 +41,14 @@ static bool rmnet_config_data_format_valid(u32 data_format)
 	u32 v5_mask = RMNET_FLAGS_INGRESS_MAP_CKSUMV5 |
 		      RMNET_FLAGS_EGRESS_MAP_CKSUMV5;
 
-	return !(data_format & v4_mask) || !(data_format & v5_mask);
+	if ((data_format & v4_mask) && (data_format & v5_mask))
+		return false;
+
+	if ((data_format & RMNET_FLAGS_INGRESS_COALESCE) &&
+	    !(data_format & RMNET_FLAGS_INGRESS_MAP_CKSUMV5))
+		return false;
+
+	return true;
 }
 
 /* Needs rtnl lock */
diff --git a/drivers/net/ethernet/qualcomm/rmnet/rmnet_handlers.c b/drivers/net/ethernet/qualcomm/rmnet/rmnet_handlers.c
index ab1dfbd833e3..bb867ceb01c1 100644
--- a/drivers/net/ethernet/qualcomm/rmnet/rmnet_handlers.c
+++ b/drivers/net/ethernet/qualcomm/rmnet/rmnet_handlers.c
@@ -96,9 +96,10 @@ __rmnet_map_ingress_handler(struct sk_buff *skb,
 
 	__skb_queue_head_init(&list);
 
-	if ((data_format & RMNET_FLAGS_INGRESS_MAP_CKSUMV5) &&
+	if ((data_format &
+	     (RMNET_FLAGS_INGRESS_MAP_CKSUMV5 | RMNET_FLAGS_INGRESS_COALESCE)) &&
 	    (map_header->flags & MAP_NEXT_HEADER_FLAG)) {
-		if (rmnet_map_process_next_hdr_packet(skb, &list, len))
+		if (rmnet_map_process_next_hdr_packet(skb, &list, len, data_format))
 			goto free_skb;
 	} else {
 		/* Subtract MAP header */
diff --git a/drivers/net/ethernet/qualcomm/rmnet/rmnet_map.h b/drivers/net/ethernet/qualcomm/rmnet/rmnet_map.h
index ef738ce015d4..77ddac6023c0 100644
--- a/drivers/net/ethernet/qualcomm/rmnet/rmnet_map.h
+++ b/drivers/net/ethernet/qualcomm/rmnet/rmnet_map.h
@@ -6,6 +6,22 @@
 #define _RMNET_MAP_H_
 #include <linux/if_rmnet.h>
 
+enum rmnet_map_v5_close_type {
+	RMNET_MAP_COAL_CLOSE_NON_COAL,
+	RMNET_MAP_COAL_CLOSE_IP_MISS,
+	RMNET_MAP_COAL_CLOSE_TRANS_MISS,
+	RMNET_MAP_COAL_CLOSE_HW,
+	RMNET_MAP_COAL_CLOSE_COAL,
+};
+
+enum rmnet_map_v5_close_value {
+	RMNET_MAP_COAL_CLOSE_HW_NL,
+	RMNET_MAP_COAL_CLOSE_HW_PKT,
+	RMNET_MAP_COAL_CLOSE_HW_BYTE,
+	RMNET_MAP_COAL_CLOSE_HW_TIME,
+	RMNET_MAP_COAL_CLOSE_HW_EVICT,
+};
+
 struct rmnet_map_control_command {
 	u8  command_name;
 	u8  cmd_type:2;
@@ -55,7 +71,7 @@ void rmnet_map_checksum_uplink_packet(struct sk_buff *skb,
 				      int csum_type);
 int rmnet_map_process_next_hdr_packet(struct sk_buff *skb,
 				      struct sk_buff_head *list,
-				      u16 len);
+				      u16 len, u32 data_format);
 unsigned int rmnet_map_tx_aggregate(struct sk_buff *skb, struct rmnet_port *port,
 				    struct net_device *orig_dev);
 void rmnet_map_tx_aggregate_init(struct rmnet_port *port);
diff --git a/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c b/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c
index 577f2758e385..5ffb811d7ef1 100644
--- a/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c
+++ b/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c
@@ -7,7 +7,11 @@
 #include <linux/netdevice.h>
 #include <linux/ip.h>
 #include <linux/ipv6.h>
+#include <linux/tcp.h>
+#include <linux/udp.h>
+#include <net/ip.h>
 #include <net/ip6_checksum.h>
+#include <net/ipv6.h>
 #include <linux/bitfield.h>
 #include "rmnet_config.h"
 #include "rmnet_map.h"
@@ -17,6 +21,19 @@
 #define RMNET_MAP_DEAGGR_SPACING  64
 #define RMNET_MAP_DEAGGR_HEADROOM (RMNET_MAP_DEAGGR_SPACING / 2)
 
+struct rmnet_map_coal_metadata {
+	void *ip_header;
+	void *trans_header;
+	u16 ip_len;
+	u16 trans_len;
+	u16 data_offset;
+	u16 data_len;
+	u8 ip_proto;
+	u8 trans_proto;
+	u8 pkt_count;
+	bool zero_csum;
+};
+
 static __sum16 *rmnet_map_get_csum_field(unsigned char protocol,
 					 const void *txporthdr)
 {
@@ -337,8 +354,8 @@ u32 rmnet_map_validate_packet_len(struct sk_buff *skb, u32 data_format)
 {
 	struct rmnet_map_v5_csum_header *next_hdr = NULL;
 	struct rmnet_map_header *maph;
-	void *data = skb->data;
 	u32 packet_len;
+	u8 hdr_type;
 
 	if (skb->len < sizeof(*maph))
 		return 0;
@@ -353,24 +370,28 @@ u32 rmnet_map_validate_packet_len(struct sk_buff *skb, u32 data_format)
 
 	if (data_format & RMNET_FLAGS_INGRESS_MAP_CKSUMV4) {
 		packet_len += sizeof(struct rmnet_map_dl_csum_trailer);
-	} else if ((data_format & RMNET_FLAGS_INGRESS_MAP_CKSUMV5) &&
+	} else if ((data_format &
+		    (RMNET_FLAGS_INGRESS_MAP_CKSUMV5 | RMNET_FLAGS_INGRESS_COALESCE)) &&
 		   !(maph->flags & MAP_CMD_FLAG)) {
-		/* Mapv5 data pkt without csum hdr is invalid */
 		if (!(maph->flags & MAP_NEXT_HEADER_FLAG))
 			return 0;
 
-		packet_len += sizeof(*next_hdr);
-		next_hdr = data + sizeof(*maph);
+		if (skb->len < sizeof(*maph) + sizeof(*next_hdr))
+			return 0;
+
+		next_hdr = (struct rmnet_map_v5_csum_header *)(skb->data + sizeof(*maph));
+		hdr_type = u8_get_bits(next_hdr->header_info,
+				       MAPV5_HDRINFO_HDR_TYPE_FMASK);
+
+		if (hdr_type == RMNET_MAP_HEADER_TYPE_CSUM_OFFLOAD)
+			packet_len += sizeof(*next_hdr);
+		else if (hdr_type != RMNET_MAP_HEADER_TYPE_COALESCING)
+			return 0;
 	}
 
 	if (skb->len < packet_len)
 		return 0;
 
-	if (next_hdr &&
-	    u8_get_bits(next_hdr->header_info, MAPV5_HDRINFO_HDR_TYPE_FMASK) !=
-	    RMNET_MAP_HEADER_TYPE_CSUM_OFFLOAD)
-		return 0;
-
 	return packet_len;
 }
 
@@ -518,13 +539,530 @@ static bool rmnet_map_get_csum_valid(struct sk_buff *skb)
 	return !!(hdr->csum_info & MAPV5_CSUMINFO_VALID_FLAG);
 }
 
+/* Stamp GSO metadata so the network stack can segment a coalesced SKB. */
+static void rmnet_map_gso_stamp(struct sk_buff *skb,
+				struct rmnet_map_coal_metadata *coal_meta)
+{
+	struct skb_shared_info *shinfo = skb_shinfo(skb);
+
+	if (coal_meta->trans_proto == IPPROTO_TCP)
+		shinfo->gso_type = (coal_meta->ip_proto == 4) ?
+				   SKB_GSO_TCPV4 : SKB_GSO_TCPV6;
+	else
+		shinfo->gso_type = SKB_GSO_UDP_L4;
+
+	shinfo->gso_size = coal_meta->data_len;
+	shinfo->gso_segs = coal_meta->pkt_count;
+}
+
+/* Set the transport checksum to the pseudo-header checksum and request
+ * partial checksum offload, letting the NIC or stack finish it.
+ */
+static void rmnet_map_partial_csum(struct sk_buff *skb,
+				   struct rmnet_map_coal_metadata *coal_meta)
+{
+	u16 pkt_len = skb->len - coal_meta->ip_len;
+	unsigned char *data = skb->data;
+	__sum16 pseudo;
+
+	if (coal_meta->ip_proto == 4) {
+		struct iphdr *iph = (struct iphdr *)data;
+
+		pseudo = ~csum_tcpudp_magic(iph->saddr, iph->daddr,
+					    pkt_len, coal_meta->trans_proto, 0);
+	} else {
+		struct ipv6hdr *ip6h = (struct ipv6hdr *)data;
+
+		pseudo = ~csum_ipv6_magic(&ip6h->saddr, &ip6h->daddr,
+					  pkt_len, coal_meta->trans_proto, 0);
+	}
+
+	if (coal_meta->trans_proto == IPPROTO_TCP) {
+		struct tcphdr *tp = (struct tcphdr *)(data + coal_meta->ip_len);
+
+		tp->check = pseudo;
+		skb->csum_offset = offsetof(struct tcphdr, check);
+	} else {
+		struct udphdr *up = (struct udphdr *)(data + coal_meta->ip_len);
+
+		up->check = pseudo;
+		skb->csum_offset = offsetof(struct udphdr, check);
+	}
+
+	skb->ip_summed = CHECKSUM_PARTIAL;
+	skb->csum_start = skb->data + coal_meta->ip_len - skb->head;
+}
+
+/* Carve one logical segment from a coalesced SKB and append it to the list.
+ * Adjusts TCP sequence numbers, IP IDs/lengths, and checksum state.
+ */
+static void
+__rmnet_map_segment_coal_skb(struct sk_buff *coal_skb,
+			     struct rmnet_map_coal_metadata *coal_meta,
+			     struct sk_buff_head *list, u8 pkt_id,
+			     bool csum_valid)
+{
+	u32 dlen = coal_meta->data_len * coal_meta->pkt_count;
+	u32 hlen = coal_meta->ip_len + coal_meta->trans_len;
+	struct sk_buff *skbn;
+
+	/* RFC 768: UDP checksum is optional for IPv4, and is 0 if unused.
+	 * Such packets are never actually bad, regardless of what the
+	 * checksum bitmap says.
+	 */
+	if (!csum_valid && coal_meta->zero_csum)
+		csum_valid = true;
+
+	if (!csum_valid)
+		goto next_pkt;
+
+	skbn = alloc_skb(hlen + dlen + RMNET_MAP_DEAGGR_HEADROOM, GFP_ATOMIC);
+	if (!skbn)
+		goto next_pkt;
+
+	skb_reserve(skbn, hlen + RMNET_MAP_DEAGGR_HEADROOM);
+	skb_put_data(skbn,
+		     coal_skb->data + coal_meta->ip_len + coal_meta->trans_len +
+		     coal_meta->data_offset,
+		     dlen);
+
+	/* Restore transport header */
+	skb_push(skbn, coal_meta->trans_len);
+	memcpy(skbn->data, coal_meta->trans_header, coal_meta->trans_len);
+	skb_reset_transport_header(skbn);
+
+	if (coal_meta->trans_proto == IPPROTO_TCP) {
+		struct tcphdr *th = tcp_hdr(skbn);
+
+		th->seq = htonl(ntohl(th->seq) + coal_meta->data_offset);
+		/* Strip dangerous flags from non-final segments */
+		if ((th->fin || th->psh) &&
+		    hlen + coal_meta->data_offset + dlen < coal_skb->len) {
+			th->fin = 0;
+			th->psh = 0;
+		}
+	} else if (coal_meta->trans_proto == IPPROTO_UDP) {
+		struct udphdr *uh = udp_hdr(skbn);
+
+		uh->len = htons(skbn->len);
+	}
+
+	/* Restore IP header */
+	skb_push(skbn, coal_meta->ip_len);
+	memcpy(skbn->data, coal_meta->ip_header, coal_meta->ip_len);
+	skb_reset_network_header(skbn);
+
+	if (coal_meta->ip_proto == 4) {
+		struct iphdr *iph = ip_hdr(skbn);
+
+		iph->id = htons(ntohs(iph->id) + pkt_id);
+		iph->tot_len = htons(skbn->len);
+		iph->check = 0;
+		iph->check = ip_fast_csum(iph, iph->ihl);
+	} else {
+		ipv6_hdr(skbn)->payload_len =
+			htons(skbn->len - sizeof(struct ipv6hdr));
+	}
+
+	rmnet_map_partial_csum(skbn, coal_meta);
+
+	skbn->dev = coal_skb->dev;
+
+	if (coal_meta->pkt_count > 1)
+		rmnet_map_gso_stamp(skbn, coal_meta);
+
+	__skb_queue_tail(list, skbn);
+
+next_pkt:
+	coal_meta->data_offset += dlen;
+	coal_meta->pkt_count = 0;
+}
+
+/* Parse the IP header of the coalesced frame and perform basic
+ * validation of header fields.
+ */
+static bool rmnet_map_coal_parse_ip_hdr(struct sk_buff *coal_skb,
+					struct rmnet_map_coal_metadata *meta,
+					bool *gro)
+{
+	struct ipv6hdr *ip6h;
+	struct iphdr *iph;
+	__be16 frag_off;
+	u8 protocol;
+	int ret;
+
+	if (coal_skb->len < sizeof(*iph))
+		return false;
+
+	iph = (struct iphdr *)coal_skb->data;
+
+	if (iph->version == 4) {
+		meta->ip_proto = 4;
+		meta->ip_len = iph->ihl * 4;
+		meta->trans_proto = iph->protocol;
+		meta->ip_header = iph;
+		if (meta->ip_len < sizeof(*iph) || coal_skb->len < meta->ip_len)
+			return false;
+
+		if (ip_is_fragment(iph))
+			return false;
+
+		if (iph->ihl != 5)
+			*gro = false;
+	} else if (iph->version == 6) {
+		if (coal_skb->len < sizeof(*ip6h))
+			return false;
+
+		ip6h = (struct ipv6hdr *)iph;
+		protocol = ip6h->nexthdr;
+		meta->ip_proto = 6;
+		ret = ipv6_skip_exthdr(coal_skb, sizeof(*ip6h), &protocol,
+				       &frag_off);
+		if (ret < 0 || frag_off || ret > U16_MAX)
+			return false;
+
+		meta->ip_len = (u16)ret;
+		meta->trans_proto = protocol;
+		meta->ip_header = ip6h;
+		if (coal_skb->len < meta->ip_len)
+			return false;
+
+		if (meta->ip_len > sizeof(*ip6h))
+			*gro = false;
+	} else {
+		return false;
+	}
+
+	return true;
+}
+
+/* Parse the transport header following the IP header into coal_meta. The
+ * available length is checked before any field is read.
+ */
+static bool rmnet_map_coal_parse_trans_hdr(struct sk_buff *coal_skb,
+					   struct rmnet_map_coal_metadata *meta)
+{
+	struct udphdr *uh;
+	struct tcphdr *th;
+	u32 avail;
+	u8 *base;
+
+	if (meta->ip_len > coal_skb->len)
+		return false;
+
+	base = (u8 *)meta->ip_header + meta->ip_len;
+	avail = coal_skb->len - meta->ip_len;
+
+	if (meta->trans_proto == IPPROTO_TCP) {
+		if (avail < sizeof(*th))
+			return false;
+
+		th = (struct tcphdr *)base;
+		meta->trans_len = th->doff * 4;
+		meta->trans_header = th;
+		if (meta->trans_len < sizeof(*th) || avail < meta->trans_len)
+			return false;
+	} else if (meta->trans_proto == IPPROTO_UDP) {
+		if (avail < sizeof(*uh))
+			return false;
+
+		uh = (struct udphdr *)base;
+		meta->trans_len = sizeof(*uh);
+		meta->trans_header = uh;
+		if (meta->ip_proto == 4 && !uh->check)
+			meta->zero_csum = true;
+	} else {
+		return false;
+	}
+
+	return true;
+}
+
+/* Reject the frame unless the total data bytes claimed by all NLOs
+ * exactly match the actual payload in the SKB. Each pkt_len covers
+ * IP+transport headers plus per-packet data. Headers appear once, so
+ * subtract hlen per packet and track the running sum against available
+ * data. An exact match is required, not just an upper bound, so that
+ * GSO metadata stamped from the NLOs stays consistent with the real
+ * SKB length.
+ */
+static bool rmnet_map_coal_validate_bounds(struct sk_buff *coal_skb,
+					   struct rmnet_map_v5_coal_header *coal_hdr,
+					   u8 num_nlos, u32 hlen, bool *gro)
+{
+	u32 total_data = 0;
+	u32 nlo_len;
+	u16 plen;
+	u8 i;
+
+	if (hlen > coal_skb->len)
+		return false;
+
+	for (i = 0; i < num_nlos; i++) {
+		plen = ntohs(coal_hdr->nl_pairs[i].pkt_len);
+
+		if (plen < hlen)
+			return false;
+
+		/* Zero-payload packets cannot use GSO with a zero gso_size. */
+		if (plen == hlen)
+			*gro = false;
+
+		nlo_len = (u32)(plen - hlen) * coal_hdr->nl_pairs[i].num_packets;
+		if (nlo_len > coal_skb->len - hlen - total_data)
+			return false;
+
+		total_data += nlo_len;
+	}
+
+	return total_data == coal_skb->len - hlen;
+}
+
+/* Attempt the GRO-friendly fast path for a single-NLO, checksum-valid frame
+ * by reusing the original SKB and stamping GSO metadata instead of copying
+ * out each segment.  Returns true if the frame was consumed via the fast
+ * path, or false if the caller should fall back to full segmentation.
+ */
+static bool rmnet_map_coal_gro_fast_path(struct sk_buff *coal_skb,
+					 struct rmnet_map_v5_coal_header *coal_hdr,
+					 struct rmnet_map_coal_metadata *coal_meta,
+					 struct sk_buff_head *list,
+					 u8 num_nlos, bool gro)
+{
+	u32 hlen = coal_meta->ip_len + coal_meta->trans_len;
+
+	if (!gro || num_nlos != 1 ||
+	    !(coal_hdr->coal_info & MAPV5_COALINFO_CSUM_VALID_FLAG))
+		return false;
+
+	coal_meta->data_len = ntohs(coal_hdr->nl_pairs[0].pkt_len) - hlen;
+	coal_meta->pkt_count = coal_hdr->nl_pairs[0].num_packets;
+
+	coal_skb->ip_summed = CHECKSUM_UNNECESSARY;
+	if (coal_meta->pkt_count > 1) {
+		rmnet_map_partial_csum(coal_skb, coal_meta);
+		rmnet_map_gso_stamp(coal_skb, coal_meta);
+	}
+
+	__skb_queue_tail(list, coal_skb);
+	return true;
+}
+
+/* NLO packet lengths are already bounds-checked by
+ * rmnet_map_coal_validate_bounds() so no further validation is needed here.
+ */
+static void rmnet_map_coal_segment_loop(struct sk_buff *coal_skb,
+					struct rmnet_map_v5_coal_header *coal_hdr,
+					struct rmnet_map_coal_metadata *coal_meta,
+					struct sk_buff_head *list,
+					u64 nlo_err_mask, bool gro, u8 num_nlos)
+{
+	u32 hlen = coal_meta->ip_len + coal_meta->trans_len;
+	u8 pkt, total_pkt = 0;
+	bool csum_err;
+	u16 pkt_len;
+	u8 nlo;
+
+	for (nlo = 0; nlo < num_nlos; nlo++) {
+		pkt_len = ntohs(coal_hdr->nl_pairs[nlo].pkt_len);
+		pkt_len -= hlen;
+		coal_meta->data_len = pkt_len;
+
+		/* nlo_err_mask is one flat bitstream across all NLOs. Shift
+		 * it once per packet in absolute frame order and do not
+		 * re-align at the NLO boundary above. See the comment on
+		 * rmnet_map_data_check_coal_header() for why.
+		 */
+		for (pkt = 0; pkt < coal_hdr->nl_pairs[nlo].num_packets;
+		     pkt++, total_pkt++, nlo_err_mask >>= 1) {
+			csum_err = nlo_err_mask & 1;
+
+			if (!gro) {
+				coal_meta->pkt_count = 1;
+				__rmnet_map_segment_coal_skb(coal_skb, coal_meta,
+							     list, total_pkt,
+							     !csum_err);
+				continue;
+			}
+
+			if (csum_err) {
+				if (coal_meta->pkt_count)
+					__rmnet_map_segment_coal_skb(coal_skb,
+								     coal_meta,
+								     list,
+								     total_pkt -
+								     coal_meta->pkt_count,
+								     true);
+				coal_meta->pkt_count = 1;
+				__rmnet_map_segment_coal_skb(coal_skb, coal_meta,
+							     list, total_pkt,
+							     false);
+			} else {
+				coal_meta->pkt_count++;
+			}
+		}
+
+		/* Flush remaining packets from this NLO */
+		if (coal_meta->pkt_count)
+			__rmnet_map_segment_coal_skb(coal_skb, coal_meta, list,
+						     total_pkt - coal_meta->pkt_count, true);
+	}
+}
+
+/* Expand a coalesced SKB into individual IP packets placed on the list.
+ * NLOs with checksum errors are dropped. __rmnet_map_ingress_handler will
+ * free the SKB in the error case.
+ */
+static int rmnet_map_segment_coal_skb(struct sk_buff *coal_skb,
+				      u64 nlo_err_mask,
+				      struct sk_buff_head *list,
+				      u16 len, u16 total_pkts)
+{
+	bool gro_hw = coal_skb->dev->features & NETIF_F_GRO_HW;
+	bool rxcsum = coal_skb->dev->features & NETIF_F_RXCSUM;
+	struct rmnet_map_v5_coal_header *coal_hdr;
+	struct rmnet_map_coal_metadata coal_meta;
+	bool gro = gro_hw;
+	u8 num_nlos;
+	u32 hlen;
+
+	memset(&coal_meta, 0, sizeof(coal_meta));
+
+	/* Drop any MAP frame padding. The coal header is counted in len */
+	skb_pull(coal_skb, sizeof(struct rmnet_map_header));
+	skb_trim(coal_skb, len);
+	coal_hdr = (struct rmnet_map_v5_coal_header *)coal_skb->data;
+	num_nlos = u8_get_bits(coal_hdr->coal_info, MAPV5_COALINFO_NUM_NLOS_FMASK);
+	skb_pull(coal_skb, sizeof(*coal_hdr));
+
+	if (!rmnet_map_coal_parse_ip_hdr(coal_skb, &coal_meta, &gro))
+		return -EINVAL;
+
+	if (!rmnet_map_coal_parse_trans_hdr(coal_skb, &coal_meta))
+		return -EINVAL;
+
+	hlen = coal_meta.ip_len + coal_meta.trans_len;
+
+	if (!rmnet_map_coal_validate_bounds(coal_skb, coal_hdr, num_nlos, hlen,
+					    &gro))
+		return -EINVAL;
+
+	/* Device capability gates coalesced delivery. Packet format can still
+	 * disable GSO and use the per-packet fallback below.
+	 */
+	if (total_pkts > 1 && (!rxcsum || !gro_hw))
+		return -EINVAL;
+
+	if (total_pkts == 1 && (!rxcsum || !gro_hw)) {
+		coal_skb->ip_summed = CHECKSUM_NONE;
+		__skb_queue_tail(list, coal_skb);
+		return 0;
+	}
+
+	if (rmnet_map_coal_gro_fast_path(coal_skb, coal_hdr, &coal_meta, list,
+					 num_nlos, gro))
+		return 0;
+
+	rmnet_map_coal_segment_loop(coal_skb, coal_hdr, &coal_meta, list,
+				    nlo_err_mask, gro, num_nlos);
+
+	return 0;
+}
+
+/* Validate the coalescing header and build the checksum error mask.
+ *
+ * Checks performed:
+ *  - MAP pkt_len accommodates the coal header (coal header is counted in
+ *    pkt_len. pkt_len < sizeof(*coal_hdr) means no payload is possible).
+ *  - num_nlos is in [1, RMNET_MAP_V5_MAX_NLOS].
+ *  - Total packet count does not exceed RMNET_MAP_V5_MAX_PACKETS.
+ *
+ * nlo_err_mask is NOT six independent per-NLO bitmaps. Each nl_pairs
+ * slot only has room for an 8 bit csum_error_bitmap, but a single NLO
+ * can carry more than 8 packets (up to RMNET_MAP_V5_MAX_PACKETS), so
+ * hardware spills a wide NLO's error bits into the csum_error_bitmap
+ * bytes of the following slots rather than truncating them. The
+ * six bitmap bytes are therefore always concatenated in slot order
+ * into one flat RMNET_MAP_V5_MAX_NLOS * 8 = RMNET_MAP_V5_MAX_PACKETS
+ * bit value, addressed by a packet's absolute position in the frame,
+ * regardless of how many NLOs are actually in use. Receivers must
+ * walk it as a single contiguous stream and must not expect it to
+ * align it at NLO boundaries.
+ */
+static int rmnet_map_data_check_coal_header(struct sk_buff *skb,
+					    u64 *nlo_err_mask,
+					    u16 *num_pkts)
+{
+	struct rmnet_map_header *maph = (struct rmnet_map_header *)skb->data;
+	struct rmnet_map_v5_coal_header *coal_hdr;
+	u8 num_nlos;
+	u16 pkts = 0;
+	u64 mask = 0;
+	int i;
+
+	/* coal header is counted in pkt_len */
+	if (ntohs(maph->pkt_len) < sizeof(*coal_hdr))
+		return -EINVAL;
+
+	coal_hdr = (struct rmnet_map_v5_coal_header *)(skb->data + sizeof(*maph));
+	num_nlos = u8_get_bits(coal_hdr->coal_info, MAPV5_COALINFO_NUM_NLOS_FMASK);
+
+	if (num_nlos == 0 || num_nlos > RMNET_MAP_V5_MAX_NLOS)
+		return -EINVAL;
+
+	for (i = 0; i < RMNET_MAP_V5_MAX_NLOS; i++) {
+		u8 err = coal_hdr->nl_pairs[i].csum_error_bitmap;
+		u8 pkt = coal_hdr->nl_pairs[i].num_packets;
+
+		mask |= ((u64)err) << (8 * i);
+		if (i < num_nlos) {
+			pkts += pkt;
+			if (pkts > RMNET_MAP_V5_MAX_PACKETS)
+				return -EINVAL;
+		}
+	}
+
+	*nlo_err_mask = mask;
+	*num_pkts = pkts;
+	return 0;
+}
+
 int rmnet_map_process_next_hdr_packet(struct sk_buff *skb,
 				      struct sk_buff_head *list,
-				      u16 len)
+				      u16 len, u32 data_format)
 {
 	struct rmnet_priv *priv = netdev_priv(skb->dev);
+	u64 nlo_err_mask;
+	u16 num_pkts;
+	int rc;
 
 	switch (rmnet_map_get_next_hdr_type(skb)) {
+	case RMNET_MAP_HEADER_TYPE_COALESCING:
+		if (!(data_format & RMNET_FLAGS_INGRESS_COALESCE))
+			return -EINVAL;
+
+		rc = rmnet_map_data_check_coal_header(skb, &nlo_err_mask,
+						      &num_pkts);
+		if (rc)
+			return rc;
+
+		/* Coalescing rewrites shared packet data and GSO metadata. */
+		if (skb_unclone(skb, GFP_ATOMIC))
+			return -ENOMEM;
+
+		rc = rmnet_map_segment_coal_skb(skb, nlo_err_mask, list, len,
+						num_pkts);
+		if (rc)
+			return rc;
+
+		if (skb_peek(list) != skb) {
+			if (skb_queue_empty(list))
+				kfree_skb(skb);
+			else
+				consume_skb(skb);
+		}
+		break;
+
 	case RMNET_MAP_HEADER_TYPE_CSUM_OFFLOAD:
 		if (unlikely(!(skb->dev->features & NETIF_F_RXCSUM))) {
 			priv->stats.csum_sw++;
diff --git a/drivers/net/ethernet/qualcomm/rmnet/rmnet_vnd.c b/drivers/net/ethernet/qualcomm/rmnet/rmnet_vnd.c
index d23f74b0aa47..b8542d2f03b2 100644
--- a/drivers/net/ethernet/qualcomm/rmnet/rmnet_vnd.c
+++ b/drivers/net/ethernet/qualcomm/rmnet/rmnet_vnd.c
@@ -331,6 +331,7 @@ int rmnet_vnd_newlink(u8 id, struct net_device *rmnet_dev,
 	rmnet_dev->hw_features = NETIF_F_RXCSUM;
 	rmnet_dev->hw_features |= NETIF_F_IP_CSUM | NETIF_F_IPV6_CSUM;
 	rmnet_dev->hw_features |= NETIF_F_SG;
+	rmnet_dev->hw_features |= NETIF_F_GRO_HW;
 
 	priv->real_dev = real_dev;
 
diff --git a/include/linux/if_rmnet.h b/include/linux/if_rmnet.h
index c44bf6e80ecb..5df4d417f3d2 100644
--- a/include/linux/if_rmnet.h
+++ b/include/linux/if_rmnet.h
@@ -70,5 +70,36 @@ struct rmnet_map_v5_csum_header {
 #define MAPV5_HDRINFO_HDR_TYPE_FMASK	GENMASK(7, 1)
 #define MAPV5_CSUMINFO_VALID_FLAG	BIT(7)
 
+#define RMNET_MAP_HEADER_TYPE_COALESCING   1
 #define RMNET_MAP_HEADER_TYPE_CSUM_OFFLOAD 2
+
+/* MAPv5 coalescing header */
+#define RMNET_MAP_V5_MAX_NLOS		6
+#define RMNET_MAP_V5_MAX_PACKETS	48
+
+/* One Number-Length Object pair: per-NLO packet count and length. */
+struct rmnet_map_v5_nl_pair {
+	__be16 pkt_len;
+	u8  csum_error_bitmap;
+	u8  num_packets;
+} __aligned(1);
+
+/* MAPv5 coalescing header: describes up to RMNET_MAP_V5_MAX_NLOS NLOs.
+ * The header immediately follows the MAP header in the frame and is
+ * included in the MAP pkt_len field.
+ */
+struct rmnet_map_v5_coal_header {
+	u8  header_info;	/* MAPV5_HDRINFO_NXT_HDR_FLAG, MAPV5_HDRINFO_HDR_TYPE_FMASK */
+	u8  coal_info;		/* MAPV5_COALINFO_* */
+	u8  close_info;		/* MAPV5_CLOSEINFO_* */
+	u8  veid_info;		/* MAPV5_VEIDINFO_* */
+	struct rmnet_map_v5_nl_pair nl_pairs[RMNET_MAP_V5_MAX_NLOS];
+} __aligned(1);
+
+#define MAPV5_COALINFO_NUM_NLOS_FMASK		GENMASK(6, 4)
+#define MAPV5_COALINFO_CSUM_VALID_FLAG		BIT(7)
+#define MAPV5_CLOSEINFO_CLOSE_TYPE_FMASK	GENMASK(3, 0)
+#define MAPV5_CLOSEINFO_CLOSE_VALUE_FMASK	GENMASK(7, 4)
+#define MAPV5_VEIDINFO_VEID_FMASK		GENMASK(3, 0)
+
 #endif /* !(_LINUX_IF_RMNET_H_) */
-- 
2.34.1


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

* [PATCH net-next v2 6/8] net: qualcomm: rmnet: Work around coalescing hardware quirks
  2026-10-08  0:55 [PATCH net-next v2 0/8] Add HW GRO handling in rmnet Subash Abhinov Kasiviswanathan
                   ` (4 preceding siblings ...)
  2026-10-08  0:55 ` [PATCH net-next v2 5/8] net: qualcomm: rmnet: Add DL packet coalescing support Subash Abhinov Kasiviswanathan
@ 2026-10-08  0:55 ` Subash Abhinov Kasiviswanathan
  2026-10-08  0:55 ` [PATCH net-next v2 7/8] net: qualcomm: rmnet: Add DL coalescing statistics Subash Abhinov Kasiviswanathan
  2026-10-08  0:55 ` [PATCH net-next v2 8/8] docs: networking: Add documentation for the coalescing support in rmnet Subash Abhinov Kasiviswanathan
  7 siblings, 0 replies; 10+ messages in thread
From: Subash Abhinov Kasiviswanathan @ 2026-10-08  0:55 UTC (permalink / raw)
  To: davem, edumazet, kuba, pabeni, andrew+netdev, corbet
  Cc: horms, skhan, rdunlap, netdev, linux-doc, linux-kernel,
	Subash Abhinov Kasiviswanathan, Sean Tranchetti

On certain hardware, the frame-level CSUM_VALID indication in a MAPv5
coalescing frame cannot be trusted when the frame contains exactly one
packet and hardware completes coalescing due to a TCP FIN or PSH flag, a
packet count limit, a byte count limit or a time limit. The GRO path could
otherwise mark the packet CHECKSUM_UNNECESSARY even when its checksum is
wrong.

Force these frames to CHECKSUM_NONE before either GRO processing or
bitmap-based segmentation. This lets the network stack verify the checksum.
Packets marked by the checksum error bitmap are therefore not dropped
directly by rmnet for these frames.

The single NLO, single packet determination this fix depends on cannot be
based on the num_nlos field declared in the coalescing header, since on
certain simulation hardware configurations that field can be reported
incorrectly even though the per-NLO num_packets fields are accurate.
Derive the effective NLO count from the contiguous num_packets prefix and
stop processing at the first empty entry. This prevents stale values in
unused slots from changing checksum fixup or segmentation behavior.

Co-developed-by: Sean Tranchetti <sean.tranchetti@oss.qualcomm.com>
Signed-off-by: Sean Tranchetti <sean.tranchetti@oss.qualcomm.com>
Signed-off-by: Subash Abhinov Kasiviswanathan <subash.a.kasiviswanathan@oss.qualcomm.com>
---
v2:
  - Update the nlo field parsing checks
  - Add more comments about the checksum related handling
v1: https://lore.kernel.org/all/20260930051345.857443-6-subash.a.kasiviswanathan@oss.qualcomm.com/ 

 .../ethernet/qualcomm/rmnet/rmnet_map_data.c  | 90 ++++++++++++++++---
 1 file changed, 80 insertions(+), 10 deletions(-)

diff --git a/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c b/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c
index 5ffb811d7ef1..e8adb4006717 100644
--- a/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c
+++ b/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c
@@ -593,6 +593,68 @@ static void rmnet_map_partial_csum(struct sk_buff *skb,
 	skb->csum_start = skb->data + coal_meta->ip_len - skb->head;
 }
 
+/* On some hardware, num_nlos in the coalescing header can be reported
+ * incorrectly under certain conditions even though the per-NLO num_packets
+ * fields it is meant to summarize are correct. Recompute the NLO count from
+ * the contiguous nl_pairs[] prefix rather than trusting the declared value.
+ * The first empty NLO marks the end of the prefix. Entries after it are not
+ * processed.
+ */
+static int rmnet_map_v5_get_num_nlos(const struct rmnet_map_v5_coal_header *coal_hdr)
+{
+	int nlos;
+
+	for (nlos = 0; nlos < RMNET_MAP_V5_MAX_NLOS; nlos++)
+		if (!coal_hdr->nl_pairs[nlos].num_packets)
+			break;
+
+	if (!nlos)
+		return -EINVAL;
+
+	return nlos;
+}
+
+static void rmnet_map_v5_set_nlos(struct rmnet_map_v5_coal_header *coal_hdr,
+				  u8 nlos)
+{
+	coal_hdr->coal_info = u8_encode_bits(nlos, MAPV5_COALINFO_NUM_NLOS_FMASK) |
+				      (coal_hdr->coal_info & MAPV5_COALINFO_CSUM_VALID_FLAG);
+}
+
+/* The frame-level CSUM_VALID indication for a single NLO, single packet
+ * coalescing frame cannot be trusted when the close reason is a TCP FIN/PSH,
+ * a packet count limit, a byte count limit or a time limit.
+ */
+static bool rmnet_map_v5_csum_fixup(struct rmnet_map_v5_coal_header *coal_hdr)
+{
+	u8 close_value = u8_get_bits(coal_hdr->close_info,
+				     MAPV5_CLOSEINFO_CLOSE_VALUE_FMASK);
+	u8 close_type = u8_get_bits(coal_hdr->close_info,
+				    MAPV5_CLOSEINFO_CLOSE_TYPE_FMASK);
+	u8 num_nlos = u8_get_bits(coal_hdr->coal_info,
+				  MAPV5_COALINFO_NUM_NLOS_FMASK);
+
+	/* Only applies to single NLO, single packet frames */
+	if (num_nlos != 1 || coal_hdr->nl_pairs[0].num_packets != 1)
+		return false;
+
+	/* TCP FIN or PSH triggered the close */
+	if (close_type == RMNET_MAP_COAL_CLOSE_COAL)
+		return true;
+
+	/* Hit a hardware limit */
+	if (close_type == RMNET_MAP_COAL_CLOSE_HW) {
+		switch (close_value) {
+		case RMNET_MAP_COAL_CLOSE_HW_PKT:
+		case RMNET_MAP_COAL_CLOSE_HW_BYTE:
+		case RMNET_MAP_COAL_CLOSE_HW_TIME:
+			return true;
+		}
+	}
+
+	return false;
+}
+
 /* Carve one logical segment from a coalesced SKB and append it to the list.
  * Adjusts TCP sequence numbers, IP IDs/lengths, and checksum state.
  */
@@ -916,14 +978,13 @@ static void rmnet_map_coal_segment_loop(struct sk_buff *coal_skb,
 static int rmnet_map_segment_coal_skb(struct sk_buff *coal_skb,
 				      u64 nlo_err_mask,
 				      struct sk_buff_head *list,
-				      u16 len, u16 total_pkts)
+				      u16 len, u16 total_pkts, u8 num_nlos)
 {
 	bool gro_hw = coal_skb->dev->features & NETIF_F_GRO_HW;
 	bool rxcsum = coal_skb->dev->features & NETIF_F_RXCSUM;
 	struct rmnet_map_v5_coal_header *coal_hdr;
 	struct rmnet_map_coal_metadata coal_meta;
 	bool gro = gro_hw;
-	u8 num_nlos;
 	u32 hlen;
 
 	memset(&coal_meta, 0, sizeof(coal_meta));
@@ -932,7 +993,7 @@ static int rmnet_map_segment_coal_skb(struct sk_buff *coal_skb,
 	skb_pull(coal_skb, sizeof(struct rmnet_map_header));
 	skb_trim(coal_skb, len);
 	coal_hdr = (struct rmnet_map_v5_coal_header *)coal_skb->data;
-	num_nlos = u8_get_bits(coal_hdr->coal_info, MAPV5_COALINFO_NUM_NLOS_FMASK);
+	rmnet_map_v5_set_nlos(coal_hdr, num_nlos);
 	skb_pull(coal_skb, sizeof(*coal_hdr));
 
 	if (!rmnet_map_coal_parse_ip_hdr(coal_skb, &coal_meta, &gro))
@@ -959,6 +1020,12 @@ static int rmnet_map_segment_coal_skb(struct sk_buff *coal_skb,
 		return 0;
 	}
 
+	if (rmnet_map_v5_csum_fixup(coal_hdr) && !coal_meta.zero_csum) {
+		coal_skb->ip_summed = CHECKSUM_NONE;
+		__skb_queue_tail(list, coal_skb);
+		return 0;
+	}
+
 	if (rmnet_map_coal_gro_fast_path(coal_skb, coal_hdr, &coal_meta, list,
 					 num_nlos, gro))
 		return 0;
@@ -991,13 +1058,14 @@ static int rmnet_map_segment_coal_skb(struct sk_buff *coal_skb,
  */
 static int rmnet_map_data_check_coal_header(struct sk_buff *skb,
 					    u64 *nlo_err_mask,
+					    u8 *num_nlos,
 					    u16 *num_pkts)
 {
 	struct rmnet_map_header *maph = (struct rmnet_map_header *)skb->data;
 	struct rmnet_map_v5_coal_header *coal_hdr;
-	u8 num_nlos;
 	u16 pkts = 0;
 	u64 mask = 0;
+	int nlos;
 	int i;
 
 	/* coal header is counted in pkt_len */
@@ -1005,17 +1073,18 @@ static int rmnet_map_data_check_coal_header(struct sk_buff *skb,
 		return -EINVAL;
 
 	coal_hdr = (struct rmnet_map_v5_coal_header *)(skb->data + sizeof(*maph));
-	num_nlos = u8_get_bits(coal_hdr->coal_info, MAPV5_COALINFO_NUM_NLOS_FMASK);
-
-	if (num_nlos == 0 || num_nlos > RMNET_MAP_V5_MAX_NLOS)
+	nlos = rmnet_map_v5_get_num_nlos(coal_hdr);
+	if (nlos < 0)
 		return -EINVAL;
 
+	*num_nlos = nlos;
+
 	for (i = 0; i < RMNET_MAP_V5_MAX_NLOS; i++) {
 		u8 err = coal_hdr->nl_pairs[i].csum_error_bitmap;
 		u8 pkt = coal_hdr->nl_pairs[i].num_packets;
 
 		mask |= ((u64)err) << (8 * i);
-		if (i < num_nlos) {
+		if (i < *num_nlos) {
 			pkts += pkt;
 			if (pkts > RMNET_MAP_V5_MAX_PACKETS)
 				return -EINVAL;
@@ -1034,6 +1103,7 @@ int rmnet_map_process_next_hdr_packet(struct sk_buff *skb,
 	struct rmnet_priv *priv = netdev_priv(skb->dev);
 	u64 nlo_err_mask;
 	u16 num_pkts;
+	u8 num_nlos;
 	int rc;
 
 	switch (rmnet_map_get_next_hdr_type(skb)) {
@@ -1042,7 +1112,7 @@ int rmnet_map_process_next_hdr_packet(struct sk_buff *skb,
 			return -EINVAL;
 
 		rc = rmnet_map_data_check_coal_header(skb, &nlo_err_mask,
-						      &num_pkts);
+						      &num_nlos, &num_pkts);
 		if (rc)
 			return rc;
 
@@ -1051,7 +1121,7 @@ int rmnet_map_process_next_hdr_packet(struct sk_buff *skb,
 			return -ENOMEM;
 
 		rc = rmnet_map_segment_coal_skb(skb, nlo_err_mask, list, len,
-						num_pkts);
+						num_pkts, num_nlos);
 		if (rc)
 			return rc;
 
-- 
2.34.1


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

* [PATCH net-next v2 7/8] net: qualcomm: rmnet: Add DL coalescing statistics
  2026-10-08  0:55 [PATCH net-next v2 0/8] Add HW GRO handling in rmnet Subash Abhinov Kasiviswanathan
                   ` (5 preceding siblings ...)
  2026-10-08  0:55 ` [PATCH net-next v2 6/8] net: qualcomm: rmnet: Work around coalescing hardware quirks Subash Abhinov Kasiviswanathan
@ 2026-10-08  0:55 ` Subash Abhinov Kasiviswanathan
  2026-10-08 23:59   ` kernel test robot
  2026-10-08  0:55 ` [PATCH net-next v2 8/8] docs: networking: Add documentation for the coalescing support in rmnet Subash Abhinov Kasiviswanathan
  7 siblings, 1 reply; 10+ messages in thread
From: Subash Abhinov Kasiviswanathan @ 2026-10-08  0:55 UTC (permalink / raw)
  To: davem, edumazet, kuba, pabeni, andrew+netdev, corbet
  Cc: horms, skhan, rdunlap, netdev, linux-doc, linux-kernel,
	Subash Abhinov Kasiviswanathan, Sean Tranchetti

Track downlink coalescing activity with standard network stack RX
statistics and rmnet specific ethtool diagnostics.

Expose these standard RX statistics through netdev_stat_ops:

- HW-GRO packets and the number of wire packets represented by them
- RX allocation failures
- RX drops for reconstructed packets discarded by rmnet

Keep these rmnet specific ethtool statistics for diagnostics that have no
standard representation:

- Coalescing header, packet count and bounds errors
- Invalid IP and transport headers
- Segment reconstruction
- Hardware close reasons

Store the per-CPU counters as u64_stats_t values and update them with
u64_stats_inc() and u64_stats_add() under u64_stats_sync protection. Copy
them into plain snapshot structures with u64_stats_copy() before
aggregation. This avoids torn counter reads during reporting.

Checksum error diagnostics distinguish hardware reported errors from
packets actually dropped. IPv4 UDP packets with a zero checksum are
accepted and are not counted as drops.

Co-developed-by: Sean Tranchetti <sean.tranchetti@oss.qualcomm.com>
Signed-off-by: Sean Tranchetti <sean.tranchetti@oss.qualcomm.com>
Signed-off-by: Subash Abhinov Kasiviswanathan <subash.a.kasiviswanathan@oss.qualcomm.com>
---
v2:
  - Use per cpu counters to avoid torn reads
  - Add network stack specific stats for HW_GRO
  - Add a build check for ethtool fields
  - Update comments and commit text explaining the stats
v1: https://lore.kernel.org/all/20260930051345.857443-7-subash.a.kasiviswanathan@oss.qualcomm.com/

 .../ethernet/qualcomm/rmnet/rmnet_config.h    |  84 ++++-
 .../ethernet/qualcomm/rmnet/rmnet_map_data.c  | 338 ++++++++++++++++--
 .../net/ethernet/qualcomm/rmnet/rmnet_vnd.c   | 124 ++++++-
 3 files changed, 499 insertions(+), 47 deletions(-)

diff --git a/drivers/net/ethernet/qualcomm/rmnet/rmnet_config.h b/drivers/net/ethernet/qualcomm/rmnet/rmnet_config.h
index 5adda0323dda..c71587c843e4 100644
--- a/drivers/net/ethernet/qualcomm/rmnet/rmnet_config.h
+++ b/drivers/net/ethernet/qualcomm/rmnet/rmnet_config.h
@@ -63,12 +63,14 @@ struct rmnet_vnd_stats {
 	u32 tx_drops;
 };
 
-struct rmnet_pcpu_stats {
-	struct rmnet_vnd_stats stats;
-	struct u64_stats_sync syncp;
-};
-
 struct rmnet_priv_stats {
+	/* Network stack RX statistics for all rmnet receive paths */
+	u64 rx_hw_gro_packets;
+	u64 rx_hw_gro_wire_packets;
+	u64 rx_alloc_fail;
+	u64 rx_dropped;
+
+	/* Custom diagnostics for the coalescing receive path */
 	u64 csum_ok;
 	u64 csum_ip4_header_bad;
 	u64 csum_valid_unset;
@@ -80,6 +82,77 @@ struct rmnet_priv_stats {
 	u64 csum_skipped;
 	u64 csum_sw;
 	u64 csum_hw;
+	/* Coalescing-only counters. Some are subsets of the standard counters. */
+	u64 coal_alloc_fail;
+	u64 coal_rx;
+	u64 coal_pkts;
+	u64 coal_hdr_err;
+	u64 coal_hdr_pkt_err;
+	u64 coal_bounds_err;
+	u64 coal_csum_err;
+	u64 coal_csum_drop;
+	u64 coal_reconstruct;
+	u64 coal_ip_invalid;
+	u64 coal_trans_invalid;
+	/* close-reason sub-counters */
+	u64 coal_close_non_coal;
+	u64 coal_close_ip_miss;
+	u64 coal_close_trans_miss;
+	u64 coal_close_hw_nl;
+	u64 coal_close_hw_pkt;
+	u64 coal_close_hw_byte;
+	u64 coal_close_hw_time;
+	u64 coal_close_hw_evict;
+	u64 coal_close_coal;
+};
+
+struct rmnet_pcpu_priv_stats {
+	/* Network stack RX statistics for all rmnet receive paths */
+	u64_stats_t rx_hw_gro_packets;
+	u64_stats_t rx_hw_gro_wire_packets;
+	u64_stats_t rx_alloc_fail;
+	u64_stats_t rx_dropped;
+
+	/* Custom diagnostics for the coalescing receive path */
+	u64_stats_t csum_ok;
+	u64_stats_t csum_ip4_header_bad;
+	u64_stats_t csum_valid_unset;
+	u64_stats_t csum_validation_failed;
+	u64_stats_t csum_err_bad_buffer;
+	u64_stats_t csum_err_invalid_ip_version;
+	u64_stats_t csum_err_invalid_transport;
+	u64_stats_t csum_fragmented_pkt;
+	u64_stats_t csum_skipped;
+	u64_stats_t csum_sw;
+	u64_stats_t csum_hw;
+	/* Coalescing-only counters. Some are subsets of the standard counters. */
+	u64_stats_t coal_alloc_fail;
+	u64_stats_t coal_rx;
+	u64_stats_t coal_pkts;
+	u64_stats_t coal_hdr_err;
+	u64_stats_t coal_hdr_pkt_err;
+	u64_stats_t coal_bounds_err;
+	u64_stats_t coal_csum_err;
+	u64_stats_t coal_csum_drop;
+	u64_stats_t coal_reconstruct;
+	u64_stats_t coal_ip_invalid;
+	u64_stats_t coal_trans_invalid;
+	/* close-reason sub-counters */
+	u64_stats_t coal_close_non_coal;
+	u64_stats_t coal_close_ip_miss;
+	u64_stats_t coal_close_trans_miss;
+	u64_stats_t coal_close_hw_nl;
+	u64_stats_t coal_close_hw_pkt;
+	u64_stats_t coal_close_hw_byte;
+	u64_stats_t coal_close_hw_time;
+	u64_stats_t coal_close_hw_evict;
+	u64_stats_t coal_close_coal;
+};
+
+struct rmnet_pcpu_stats {
+	struct rmnet_vnd_stats stats;
+	struct rmnet_pcpu_priv_stats priv_stats;
+	struct u64_stats_sync syncp;
 };
 
 struct rmnet_priv {
@@ -87,7 +160,6 @@ struct rmnet_priv {
 	struct net_device *real_dev;
 	struct rmnet_pcpu_stats __percpu *pcpu_stats;
 	struct gro_cells gro_cells;
-	struct rmnet_priv_stats stats;
 };
 
 struct rmnet_port *rmnet_get_port_rcu(const struct net_device *real_dev);
diff --git a/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c b/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c
index e8adb4006717..74d64b7066e2 100644
--- a/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c
+++ b/drivers/net/ethernet/qualcomm/rmnet/rmnet_map_data.c
@@ -53,34 +53,45 @@ rmnet_map_ipv4_dl_csum_trailer(struct sk_buff *skb,
 {
 	struct iphdr *ip4h = (struct iphdr *)skb->data;
 	void *txporthdr = skb->data + ip4h->ihl * 4;
+	struct rmnet_pcpu_stats *pcpu_ptr;
 	__sum16 *csum_field, pseudo_csum;
 	__sum16 ip_payload_csum;
 
+	pcpu_ptr = this_cpu_ptr(priv->pcpu_stats);
+
 	/* Computing the checksum over just the IPv4 header--including its
 	 * checksum field--should yield 0.  If it doesn't, the IP header
 	 * is bad, so return an error and let the IP layer drop it.
 	 */
 	if (ip_fast_csum(ip4h, ip4h->ihl)) {
-		priv->stats.csum_ip4_header_bad++;
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.csum_ip4_header_bad);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return -EINVAL;
 	}
 
 	/* We don't support checksum offload on IPv4 fragments */
 	if (ip_is_fragment(ip4h)) {
-		priv->stats.csum_fragmented_pkt++;
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.csum_fragmented_pkt);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return -EOPNOTSUPP;
 	}
 
 	/* Checksum offload is only supported for UDP and TCP protocols */
 	csum_field = rmnet_map_get_csum_field(ip4h->protocol, txporthdr);
 	if (!csum_field) {
-		priv->stats.csum_err_invalid_transport++;
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.csum_err_invalid_transport);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return -EPROTONOSUPPORT;
 	}
 
 	/* RFC 768: UDP checksum is optional for IPv4, and is 0 if unused */
 	if (!*csum_field && ip4h->protocol == IPPROTO_UDP) {
-		priv->stats.csum_skipped++;
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.csum_skipped);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return 0;
 	}
 
@@ -113,11 +124,15 @@ rmnet_map_ipv4_dl_csum_trailer(struct sk_buff *skb,
 
 	/* The cast is required to ensure only the low 16 bits are examined */
 	if (ip_payload_csum != (__sum16)~pseudo_csum) {
-		priv->stats.csum_validation_failed++;
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.csum_validation_failed);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return -EINVAL;
 	}
 
-	priv->stats.csum_ok++;
+	u64_stats_update_begin(&pcpu_ptr->syncp);
+	u64_stats_inc(&pcpu_ptr->priv_stats.csum_ok);
+	u64_stats_update_end(&pcpu_ptr->syncp);
 	return 0;
 }
 
@@ -129,16 +144,21 @@ rmnet_map_ipv6_dl_csum_trailer(struct sk_buff *skb,
 {
 	struct ipv6hdr *ip6h = (struct ipv6hdr *)skb->data;
 	void *txporthdr = skb->data + sizeof(*ip6h);
+	struct rmnet_pcpu_stats *pcpu_ptr;
 	__sum16 *csum_field, pseudo_csum;
 	__sum16 ip6_payload_csum;
 	__be16 ip_header_csum;
 
+	pcpu_ptr = this_cpu_ptr(priv->pcpu_stats);
+
 	/* Checksum offload is only supported for UDP and TCP protocols;
 	 * the packet cannot include any IPv6 extension headers
 	 */
 	csum_field = rmnet_map_get_csum_field(ip6h->nexthdr, txporthdr);
 	if (!csum_field) {
-		priv->stats.csum_err_invalid_transport++;
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.csum_err_invalid_transport);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return -EPROTONOSUPPORT;
 	}
 
@@ -164,11 +184,15 @@ rmnet_map_ipv6_dl_csum_trailer(struct sk_buff *skb,
 	 * examined.
 	 */
 	if (ip6_payload_csum != (__sum16)~pseudo_csum) {
-		priv->stats.csum_validation_failed++;
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.csum_validation_failed);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return -EINVAL;
 	}
 
-	priv->stats.csum_ok++;
+	u64_stats_update_begin(&pcpu_ptr->syncp);
+	u64_stats_inc(&pcpu_ptr->priv_stats.csum_ok);
+	u64_stats_update_end(&pcpu_ptr->syncp);
 	return 0;
 }
 #else
@@ -263,6 +287,9 @@ static void rmnet_map_v5_checksum_uplink_packet(struct sk_buff *skb,
 {
 	struct rmnet_priv *priv = netdev_priv(orig_dev);
 	struct rmnet_map_v5_csum_header *ul_header;
+	struct rmnet_pcpu_stats *pcpu_ptr;
+
+	pcpu_ptr = this_cpu_ptr(priv->pcpu_stats);
 
 	ul_header = skb_push(skb, sizeof(*ul_header));
 	memset(ul_header, 0, sizeof(*ul_header));
@@ -287,7 +314,9 @@ static void rmnet_map_v5_checksum_uplink_packet(struct sk_buff *skb,
 			proto = ((struct ipv6hdr *)iph)->nexthdr;
 			trans = iph + ip_len;
 		} else {
-			priv->stats.csum_err_invalid_ip_version++;
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.csum_err_invalid_ip_version);
+			u64_stats_update_end(&pcpu_ptr->syncp);
 			goto sw_csum;
 		}
 
@@ -296,13 +325,17 @@ static void rmnet_map_v5_checksum_uplink_packet(struct sk_buff *skb,
 			skb->ip_summed = CHECKSUM_NONE;
 			/* Ask for checksum offloading */
 			ul_header->csum_info |= MAPV5_CSUMINFO_VALID_FLAG;
-			priv->stats.csum_hw++;
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.csum_hw);
+			u64_stats_update_end(&pcpu_ptr->syncp);
 			return;
 		}
 	}
 
 sw_csum:
-	priv->stats.csum_sw++;
+	u64_stats_update_begin(&pcpu_ptr->syncp);
+	u64_stats_inc(&pcpu_ptr->priv_stats.csum_sw);
+	u64_stats_update_end(&pcpu_ptr->syncp);
 }
 
 /* Adds MAP header to front of skb->data
@@ -434,16 +467,23 @@ int rmnet_map_checksum_downlink_packet(struct sk_buff *skb, u16 len)
 {
 	struct rmnet_priv *priv = netdev_priv(skb->dev);
 	struct rmnet_map_dl_csum_trailer *csum_trailer;
+	struct rmnet_pcpu_stats *pcpu_ptr;
+
+	pcpu_ptr = this_cpu_ptr(priv->pcpu_stats);
 
 	if (unlikely(!(skb->dev->features & NETIF_F_RXCSUM))) {
-		priv->stats.csum_sw++;
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.csum_sw);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return -EOPNOTSUPP;
 	}
 
 	csum_trailer = (struct rmnet_map_dl_csum_trailer *)(skb->data + len);
 
 	if (!(csum_trailer->flags & MAP_CSUM_DL_VALID_FLAG)) {
-		priv->stats.csum_valid_unset++;
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.csum_valid_unset);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return -EINVAL;
 	}
 
@@ -453,7 +493,9 @@ int rmnet_map_checksum_downlink_packet(struct sk_buff *skb, u16 len)
 	if (IS_ENABLED(CONFIG_IPV6) && skb->protocol == htons(ETH_P_IPV6))
 		return rmnet_map_ipv6_dl_csum_trailer(skb, csum_trailer, priv);
 
-	priv->stats.csum_err_invalid_ip_version++;
+	u64_stats_update_begin(&pcpu_ptr->syncp);
+	u64_stats_inc(&pcpu_ptr->priv_stats.csum_err_invalid_ip_version);
+	u64_stats_update_end(&pcpu_ptr->syncp);
 
 	return -EPROTONOSUPPORT;
 }
@@ -463,8 +505,11 @@ static void rmnet_map_v4_checksum_uplink_packet(struct sk_buff *skb,
 {
 	struct rmnet_priv *priv = netdev_priv(orig_dev);
 	struct rmnet_map_ul_csum_header *ul_header;
+	struct rmnet_pcpu_stats *pcpu_ptr;
 	void *iphdr;
 
+	pcpu_ptr = this_cpu_ptr(priv->pcpu_stats);
+
 	ul_header = (struct rmnet_map_ul_csum_header *)
 		    skb_push(skb, sizeof(struct rmnet_map_ul_csum_header));
 
@@ -480,22 +525,30 @@ static void rmnet_map_v4_checksum_uplink_packet(struct sk_buff *skb,
 
 	if (skb->protocol == htons(ETH_P_IP)) {
 		rmnet_map_ipv4_ul_csum_header(iphdr, ul_header, skb);
-		priv->stats.csum_hw++;
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.csum_hw);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return;
 	}
 
 	if (IS_ENABLED(CONFIG_IPV6) && skb->protocol == htons(ETH_P_IPV6)) {
 		rmnet_map_ipv6_ul_csum_header(iphdr, ul_header, skb);
-		priv->stats.csum_hw++;
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.csum_hw);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return;
 	}
 
-	priv->stats.csum_err_invalid_ip_version++;
+	u64_stats_update_begin(&pcpu_ptr->syncp);
+	u64_stats_inc(&pcpu_ptr->priv_stats.csum_err_invalid_ip_version);
+	u64_stats_update_end(&pcpu_ptr->syncp);
 
 sw_csum:
 	memset(ul_header, 0, sizeof(*ul_header));
 
-	priv->stats.csum_sw++;
+	u64_stats_update_begin(&pcpu_ptr->syncp);
+	u64_stats_inc(&pcpu_ptr->priv_stats.csum_sw);
+	u64_stats_update_end(&pcpu_ptr->syncp);
 }
 
 /* Generates UL checksum meta info header for IPv4 and IPv6 over TCP and UDP
@@ -665,9 +718,13 @@ __rmnet_map_segment_coal_skb(struct sk_buff *coal_skb,
 			     bool csum_valid)
 {
 	u32 dlen = coal_meta->data_len * coal_meta->pkt_count;
+	struct rmnet_priv *priv = netdev_priv(coal_skb->dev);
 	u32 hlen = coal_meta->ip_len + coal_meta->trans_len;
+	struct rmnet_pcpu_stats *pcpu_ptr;
 	struct sk_buff *skbn;
 
+	pcpu_ptr = this_cpu_ptr(priv->pcpu_stats);
+
 	/* RFC 768: UDP checksum is optional for IPv4, and is 0 if unused.
 	 * Such packets are never actually bad, regardless of what the
 	 * checksum bitmap says.
@@ -675,12 +732,29 @@ __rmnet_map_segment_coal_skb(struct sk_buff *coal_skb,
 	if (!csum_valid && coal_meta->zero_csum)
 		csum_valid = true;
 
-	if (!csum_valid)
+	if (!csum_valid) {
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		/* rx_dropped is standard accounting. coal_csum_drop is its
+		 * coalescing-specific checksum error subset.
+		 */
+		u64_stats_add(&pcpu_ptr->priv_stats.rx_dropped, coal_meta->pkt_count);
+		u64_stats_inc(&pcpu_ptr->priv_stats.coal_csum_drop);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		goto next_pkt;
+	}
 
 	skbn = alloc_skb(hlen + dlen + RMNET_MAP_DEAGGR_HEADROOM, GFP_ATOMIC);
-	if (!skbn)
+	if (!skbn) {
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		/* The coal counter is the coalescing subset of the standard
+		 * allocation failure and RX drop counters.
+		 */
+		u64_stats_add(&pcpu_ptr->priv_stats.rx_alloc_fail, coal_meta->pkt_count);
+		u64_stats_add(&pcpu_ptr->priv_stats.rx_dropped, coal_meta->pkt_count);
+		u64_stats_add(&pcpu_ptr->priv_stats.coal_alloc_fail, coal_meta->pkt_count);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		goto next_pkt;
+	}
 
 	skb_reserve(skbn, hlen + RMNET_MAP_DEAGGR_HEADROOM);
 	skb_put_data(skbn,
@@ -729,6 +803,9 @@ __rmnet_map_segment_coal_skb(struct sk_buff *coal_skb,
 	rmnet_map_partial_csum(skbn, coal_meta);
 
 	skbn->dev = coal_skb->dev;
+	u64_stats_update_begin(&pcpu_ptr->syncp);
+	u64_stats_inc(&pcpu_ptr->priv_stats.coal_reconstruct);
+	u64_stats_update_end(&pcpu_ptr->syncp);
 
 	if (coal_meta->pkt_count > 1)
 		rmnet_map_gso_stamp(skbn, coal_meta);
@@ -747,14 +824,22 @@ static bool rmnet_map_coal_parse_ip_hdr(struct sk_buff *coal_skb,
 					struct rmnet_map_coal_metadata *meta,
 					bool *gro)
 {
+	struct rmnet_priv *priv = netdev_priv(coal_skb->dev);
+	struct rmnet_pcpu_stats *pcpu_ptr;
 	struct ipv6hdr *ip6h;
 	struct iphdr *iph;
 	__be16 frag_off;
 	u8 protocol;
 	int ret;
 
-	if (coal_skb->len < sizeof(*iph))
+	pcpu_ptr = this_cpu_ptr(priv->pcpu_stats);
+
+	if (coal_skb->len < sizeof(*iph)) {
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.coal_ip_invalid);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return false;
+	}
 
 	iph = (struct iphdr *)coal_skb->data;
 
@@ -763,35 +848,58 @@ static bool rmnet_map_coal_parse_ip_hdr(struct sk_buff *coal_skb,
 		meta->ip_len = iph->ihl * 4;
 		meta->trans_proto = iph->protocol;
 		meta->ip_header = iph;
-		if (meta->ip_len < sizeof(*iph) || coal_skb->len < meta->ip_len)
+		if (meta->ip_len < sizeof(*iph) || coal_skb->len < meta->ip_len) {
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.coal_ip_invalid);
+			u64_stats_update_end(&pcpu_ptr->syncp);
 			return false;
+		}
 
-		if (ip_is_fragment(iph))
+		if (ip_is_fragment(iph)) {
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.coal_ip_invalid);
+			u64_stats_update_end(&pcpu_ptr->syncp);
 			return false;
+		}
 
 		if (iph->ihl != 5)
 			*gro = false;
 	} else if (iph->version == 6) {
-		if (coal_skb->len < sizeof(*ip6h))
+		if (coal_skb->len < sizeof(*ip6h)) {
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.coal_ip_invalid);
+			u64_stats_update_end(&pcpu_ptr->syncp);
 			return false;
+		}
 
 		ip6h = (struct ipv6hdr *)iph;
 		protocol = ip6h->nexthdr;
 		meta->ip_proto = 6;
 		ret = ipv6_skip_exthdr(coal_skb, sizeof(*ip6h), &protocol,
 				       &frag_off);
-		if (ret < 0 || frag_off || ret > U16_MAX)
+		if (ret < 0 || frag_off || ret > U16_MAX) {
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.coal_ip_invalid);
+			u64_stats_update_end(&pcpu_ptr->syncp);
 			return false;
+		}
 
 		meta->ip_len = (u16)ret;
 		meta->trans_proto = protocol;
 		meta->ip_header = ip6h;
-		if (coal_skb->len < meta->ip_len)
+		if (coal_skb->len < meta->ip_len) {
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.coal_ip_invalid);
+			u64_stats_update_end(&pcpu_ptr->syncp);
 			return false;
+		}
 
 		if (meta->ip_len > sizeof(*ip6h))
 			*gro = false;
 	} else {
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.coal_ip_invalid);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return false;
 	}
 
@@ -804,29 +912,49 @@ static bool rmnet_map_coal_parse_ip_hdr(struct sk_buff *coal_skb,
 static bool rmnet_map_coal_parse_trans_hdr(struct sk_buff *coal_skb,
 					   struct rmnet_map_coal_metadata *meta)
 {
+	struct rmnet_priv *priv = netdev_priv(coal_skb->dev);
+	struct rmnet_pcpu_stats *pcpu_ptr;
 	struct udphdr *uh;
 	struct tcphdr *th;
 	u32 avail;
 	u8 *base;
 
-	if (meta->ip_len > coal_skb->len)
+	pcpu_ptr = this_cpu_ptr(priv->pcpu_stats);
+
+	if (meta->ip_len > coal_skb->len) {
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.coal_trans_invalid);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return false;
+	}
 
 	base = (u8 *)meta->ip_header + meta->ip_len;
 	avail = coal_skb->len - meta->ip_len;
 
 	if (meta->trans_proto == IPPROTO_TCP) {
-		if (avail < sizeof(*th))
+		if (avail < sizeof(*th)) {
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.coal_trans_invalid);
+			u64_stats_update_end(&pcpu_ptr->syncp);
 			return false;
+		}
 
 		th = (struct tcphdr *)base;
 		meta->trans_len = th->doff * 4;
 		meta->trans_header = th;
-		if (meta->trans_len < sizeof(*th) || avail < meta->trans_len)
+		if (meta->trans_len < sizeof(*th) || avail < meta->trans_len) {
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.coal_trans_invalid);
+			u64_stats_update_end(&pcpu_ptr->syncp);
 			return false;
+		}
 	} else if (meta->trans_proto == IPPROTO_UDP) {
-		if (avail < sizeof(*uh))
+		if (avail < sizeof(*uh)) {
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.coal_trans_invalid);
+			u64_stats_update_end(&pcpu_ptr->syncp);
 			return false;
+		}
 
 		uh = (struct udphdr *)base;
 		meta->trans_len = sizeof(*uh);
@@ -834,6 +962,9 @@ static bool rmnet_map_coal_parse_trans_hdr(struct sk_buff *coal_skb,
 		if (meta->ip_proto == 4 && !uh->check)
 			meta->zero_csum = true;
 	} else {
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.coal_trans_invalid);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return false;
 	}
 
@@ -919,12 +1050,16 @@ static void rmnet_map_coal_segment_loop(struct sk_buff *coal_skb,
 					struct sk_buff_head *list,
 					u64 nlo_err_mask, bool gro, u8 num_nlos)
 {
+	struct rmnet_priv *priv = netdev_priv(coal_skb->dev);
 	u32 hlen = coal_meta->ip_len + coal_meta->trans_len;
+	struct rmnet_pcpu_stats *pcpu_ptr;
 	u8 pkt, total_pkt = 0;
 	bool csum_err;
 	u16 pkt_len;
 	u8 nlo;
 
+	pcpu_ptr = this_cpu_ptr(priv->pcpu_stats);
+
 	for (nlo = 0; nlo < num_nlos; nlo++) {
 		pkt_len = ntohs(coal_hdr->nl_pairs[nlo].pkt_len);
 		pkt_len -= hlen;
@@ -939,6 +1074,12 @@ static void rmnet_map_coal_segment_loop(struct sk_buff *coal_skb,
 		     pkt++, total_pkt++, nlo_err_mask >>= 1) {
 			csum_err = nlo_err_mask & 1;
 
+			if (csum_err) {
+				u64_stats_update_begin(&pcpu_ptr->syncp);
+				u64_stats_inc(&pcpu_ptr->priv_stats.coal_csum_err);
+				u64_stats_update_end(&pcpu_ptr->syncp);
+			}
+
 			if (!gro) {
 				coal_meta->pkt_count = 1;
 				__rmnet_map_segment_coal_skb(coal_skb, coal_meta,
@@ -982,11 +1123,15 @@ static int rmnet_map_segment_coal_skb(struct sk_buff *coal_skb,
 {
 	bool gro_hw = coal_skb->dev->features & NETIF_F_GRO_HW;
 	bool rxcsum = coal_skb->dev->features & NETIF_F_RXCSUM;
+	struct rmnet_priv *priv = netdev_priv(coal_skb->dev);
 	struct rmnet_map_v5_coal_header *coal_hdr;
 	struct rmnet_map_coal_metadata coal_meta;
+	struct rmnet_pcpu_stats *pcpu_ptr;
 	bool gro = gro_hw;
 	u32 hlen;
 
+	pcpu_ptr = this_cpu_ptr(priv->pcpu_stats);
+
 	memset(&coal_meta, 0, sizeof(coal_meta));
 
 	/* Drop any MAP frame padding. The coal header is counted in len */
@@ -1005,8 +1150,12 @@ static int rmnet_map_segment_coal_skb(struct sk_buff *coal_skb,
 	hlen = coal_meta.ip_len + coal_meta.trans_len;
 
 	if (!rmnet_map_coal_validate_bounds(coal_skb, coal_hdr, num_nlos, hlen,
-					    &gro))
+					    &gro)) {
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.coal_bounds_err);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return -EINVAL;
+	}
 
 	/* Device capability gates coalesced delivery. Packet format can still
 	 * disable GSO and use the per-packet fallback below.
@@ -1014,6 +1163,16 @@ static int rmnet_map_segment_coal_skb(struct sk_buff *coal_skb,
 	if (total_pkts > 1 && (!rxcsum || !gro_hw))
 		return -EINVAL;
 
+	if (gro && total_pkts > 1) {
+		/* Standard counters cover delivered HW-GRO skbs. coal_rx and
+		 * coal_pkts cover coalescing input regardless of HW-GRO.
+		 */
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.rx_hw_gro_packets);
+		u64_stats_add(&pcpu_ptr->priv_stats.rx_hw_gro_wire_packets, total_pkts);
+		u64_stats_update_end(&pcpu_ptr->syncp);
+	}
+
 	if (total_pkts == 1 && (!rxcsum || !gro_hw)) {
 		coal_skb->ip_summed = CHECKSUM_NONE;
 		__skb_queue_tail(list, coal_skb);
@@ -1036,6 +1195,67 @@ static int rmnet_map_segment_coal_skb(struct sk_buff *coal_skb,
 	return 0;
 }
 
+/* Log the hardware close-reason counter for a coalescing header. */
+static void rmnet_map_data_log_close_stats(struct rmnet_pcpu_stats *pcpu_ptr,
+					   u8 type, u8 code)
+{
+	switch (type) {
+	case RMNET_MAP_COAL_CLOSE_NON_COAL:
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.coal_close_non_coal);
+		u64_stats_update_end(&pcpu_ptr->syncp);
+		break;
+	case RMNET_MAP_COAL_CLOSE_IP_MISS:
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.coal_close_ip_miss);
+		u64_stats_update_end(&pcpu_ptr->syncp);
+		break;
+	case RMNET_MAP_COAL_CLOSE_TRANS_MISS:
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.coal_close_trans_miss);
+		u64_stats_update_end(&pcpu_ptr->syncp);
+		break;
+	case RMNET_MAP_COAL_CLOSE_HW:
+		switch (code) {
+		case RMNET_MAP_COAL_CLOSE_HW_NL:
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.coal_close_hw_nl);
+			u64_stats_update_end(&pcpu_ptr->syncp);
+			break;
+		case RMNET_MAP_COAL_CLOSE_HW_PKT:
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.coal_close_hw_pkt);
+			u64_stats_update_end(&pcpu_ptr->syncp);
+			break;
+		case RMNET_MAP_COAL_CLOSE_HW_BYTE:
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.coal_close_hw_byte);
+			u64_stats_update_end(&pcpu_ptr->syncp);
+			break;
+		case RMNET_MAP_COAL_CLOSE_HW_TIME:
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.coal_close_hw_time);
+			u64_stats_update_end(&pcpu_ptr->syncp);
+			break;
+		case RMNET_MAP_COAL_CLOSE_HW_EVICT:
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.coal_close_hw_evict);
+			u64_stats_update_end(&pcpu_ptr->syncp);
+			break;
+		default:
+			break;
+		}
+		break;
+	case RMNET_MAP_COAL_CLOSE_COAL:
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.coal_close_coal);
+		u64_stats_update_end(&pcpu_ptr->syncp);
+		break;
+	default:
+		break;
+	}
+}
+
 /* Validate the coalescing header and build the checksum error mask.
  *
  * Checks performed:
@@ -1062,23 +1282,34 @@ static int rmnet_map_data_check_coal_header(struct sk_buff *skb,
 					    u16 *num_pkts)
 {
 	struct rmnet_map_header *maph = (struct rmnet_map_header *)skb->data;
+	struct rmnet_priv *priv = netdev_priv(skb->dev);
 	struct rmnet_map_v5_coal_header *coal_hdr;
+	struct rmnet_pcpu_stats *pcpu_ptr;
 	u16 pkts = 0;
 	u64 mask = 0;
 	int nlos;
 	int i;
 
+	pcpu_ptr = this_cpu_ptr(priv->pcpu_stats);
+
 	/* coal header is counted in pkt_len */
-	if (ntohs(maph->pkt_len) < sizeof(*coal_hdr))
+	if (ntohs(maph->pkt_len) < sizeof(*coal_hdr)) {
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.coal_hdr_err);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return -EINVAL;
+	}
 
 	coal_hdr = (struct rmnet_map_v5_coal_header *)(skb->data + sizeof(*maph));
 	nlos = rmnet_map_v5_get_num_nlos(coal_hdr);
-	if (nlos < 0)
+	if (nlos < 0) {
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.coal_hdr_err);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		return -EINVAL;
+	}
 
 	*num_nlos = nlos;
-
 	for (i = 0; i < RMNET_MAP_V5_MAX_NLOS; i++) {
 		u8 err = coal_hdr->nl_pairs[i].csum_error_bitmap;
 		u8 pkt = coal_hdr->nl_pairs[i].num_packets;
@@ -1086,11 +1317,27 @@ static int rmnet_map_data_check_coal_header(struct sk_buff *skb,
 		mask |= ((u64)err) << (8 * i);
 		if (i < *num_nlos) {
 			pkts += pkt;
-			if (pkts > RMNET_MAP_V5_MAX_PACKETS)
+			if (pkts > RMNET_MAP_V5_MAX_PACKETS) {
+				u64_stats_update_begin(&pcpu_ptr->syncp);
+				u64_stats_inc(&pcpu_ptr->priv_stats.coal_hdr_pkt_err);
+				u64_stats_update_end(&pcpu_ptr->syncp);
 				return -EINVAL;
+			}
 		}
 	}
 
+	/* coal_pkts counts packets reported by the hardware, independent of
+	 * whether the frame is later delivered through HW-GRO.
+	 */
+	u64_stats_update_begin(&pcpu_ptr->syncp);
+	u64_stats_add(&pcpu_ptr->priv_stats.coal_pkts, pkts);
+	u64_stats_update_end(&pcpu_ptr->syncp);
+	rmnet_map_data_log_close_stats(pcpu_ptr,
+				       u8_get_bits(coal_hdr->close_info,
+						   MAPV5_CLOSEINFO_CLOSE_TYPE_FMASK),
+				       u8_get_bits(coal_hdr->close_info,
+						   MAPV5_CLOSEINFO_CLOSE_VALUE_FMASK));
+
 	*nlo_err_mask = mask;
 	*num_pkts = pkts;
 	return 0;
@@ -1101,16 +1348,23 @@ int rmnet_map_process_next_hdr_packet(struct sk_buff *skb,
 				      u16 len, u32 data_format)
 {
 	struct rmnet_priv *priv = netdev_priv(skb->dev);
+	struct rmnet_pcpu_stats *pcpu_ptr;
 	u64 nlo_err_mask;
 	u16 num_pkts;
 	u8 num_nlos;
 	int rc;
 
+	pcpu_ptr = this_cpu_ptr(priv->pcpu_stats);
+
 	switch (rmnet_map_get_next_hdr_type(skb)) {
 	case RMNET_MAP_HEADER_TYPE_COALESCING:
 		if (!(data_format & RMNET_FLAGS_INGRESS_COALESCE))
 			return -EINVAL;
 
+		/* coal_rx counts coalescing input, not delivered HW-GRO skbs. */
+		u64_stats_update_begin(&pcpu_ptr->syncp);
+		u64_stats_inc(&pcpu_ptr->priv_stats.coal_rx);
+		u64_stats_update_end(&pcpu_ptr->syncp);
 		rc = rmnet_map_data_check_coal_header(skb, &nlo_err_mask,
 						      &num_nlos, &num_pkts);
 		if (rc)
@@ -1135,12 +1389,18 @@ int rmnet_map_process_next_hdr_packet(struct sk_buff *skb,
 
 	case RMNET_MAP_HEADER_TYPE_CSUM_OFFLOAD:
 		if (unlikely(!(skb->dev->features & NETIF_F_RXCSUM))) {
-			priv->stats.csum_sw++;
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.csum_sw);
+			u64_stats_update_end(&pcpu_ptr->syncp);
 		} else if (rmnet_map_get_csum_valid(skb)) {
-			priv->stats.csum_ok++;
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.csum_ok);
+			u64_stats_update_end(&pcpu_ptr->syncp);
 			skb->ip_summed = CHECKSUM_UNNECESSARY;
 		} else {
-			priv->stats.csum_valid_unset++;
+			u64_stats_update_begin(&pcpu_ptr->syncp);
+			u64_stats_inc(&pcpu_ptr->priv_stats.csum_valid_unset);
+			u64_stats_update_end(&pcpu_ptr->syncp);
 		}
 
 		skb_pull(skb, sizeof(struct rmnet_map_header) +
diff --git a/drivers/net/ethernet/qualcomm/rmnet/rmnet_vnd.c b/drivers/net/ethernet/qualcomm/rmnet/rmnet_vnd.c
index b8542d2f03b2..8e2a83ca0235 100644
--- a/drivers/net/ethernet/qualcomm/rmnet/rmnet_vnd.c
+++ b/drivers/net/ethernet/qualcomm/rmnet/rmnet_vnd.c
@@ -7,6 +7,7 @@
 #include <linux/capability.h>
 #include <linux/etherdevice.h>
 #include <linux/ethtool.h>
+#include <net/netdev_queues.h>
 #include <linux/if_arp.h>
 #include <linux/netlink.h>
 #include <net/pkt_sched.h>
@@ -141,7 +142,9 @@ static void rmnet_vnd_uninit(struct net_device *dev)
 static void rmnet_get_stats64(struct net_device *dev,
 			      struct rtnl_link_stats64 *s)
 {
+	struct rmnet_priv_stats total_priv_stats = { };
 	struct rmnet_priv *priv = netdev_priv(dev);
+	struct rmnet_priv_stats priv_snapshot;
 	struct rmnet_vnd_stats total_stats = { };
 	struct rmnet_pcpu_stats *pcpu_ptr;
 	struct rmnet_vnd_stats snapshot;
@@ -153,6 +156,8 @@ static void rmnet_get_stats64(struct net_device *dev,
 		do {
 			start = u64_stats_fetch_begin(&pcpu_ptr->syncp);
 			snapshot = pcpu_ptr->stats;	/* struct assignment */
+			u64_stats_copy(&priv_snapshot, &pcpu_ptr->priv_stats,
+				       sizeof(priv_snapshot));
 		} while (u64_stats_fetch_retry(&pcpu_ptr->syncp, start));
 
 		total_stats.rx_pkts += snapshot.rx_pkts;
@@ -160,6 +165,7 @@ static void rmnet_get_stats64(struct net_device *dev,
 		total_stats.tx_pkts += snapshot.tx_pkts;
 		total_stats.tx_bytes += snapshot.tx_bytes;
 		total_stats.tx_drops += snapshot.tx_drops;
+		total_priv_stats.rx_dropped += priv_snapshot.rx_dropped;
 	}
 
 	s->rx_packets = total_stats.rx_pkts;
@@ -167,8 +173,45 @@ static void rmnet_get_stats64(struct net_device *dev,
 	s->tx_packets = total_stats.tx_pkts;
 	s->tx_bytes = total_stats.tx_bytes;
 	s->tx_dropped = total_stats.tx_drops;
+	s->rx_dropped = total_priv_stats.rx_dropped;
 }
 
+/* Report standard network stack RX statistics. These are distinct from the
+ * rmnet-specific coalescing diagnostics exposed through ethtool.
+ */
+static void rmnet_get_queue_stats_rx(struct net_device *dev, int idx,
+				     struct netdev_queue_stats_rx *stats)
+{
+	struct rmnet_priv *priv = netdev_priv(dev);
+	struct rmnet_priv_stats total_stats = { };
+	struct rmnet_pcpu_stats *pcpu_ptr;
+	struct rmnet_priv_stats snapshot;
+	unsigned int cpu, start;
+
+	for_each_possible_cpu(cpu) {
+		pcpu_ptr = per_cpu_ptr(priv->pcpu_stats, cpu);
+
+		do {
+			start = u64_stats_fetch_begin(&pcpu_ptr->syncp);
+			u64_stats_copy(&snapshot, &pcpu_ptr->priv_stats,
+				       sizeof(snapshot));
+		} while (u64_stats_fetch_retry(&pcpu_ptr->syncp, start));
+
+		total_stats.rx_hw_gro_packets += snapshot.rx_hw_gro_packets;
+		total_stats.rx_hw_gro_wire_packets +=
+			snapshot.rx_hw_gro_wire_packets;
+		total_stats.rx_alloc_fail += snapshot.rx_alloc_fail;
+	}
+
+	stats->hw_gro_packets = total_stats.rx_hw_gro_packets;
+	stats->hw_gro_wire_packets = total_stats.rx_hw_gro_wire_packets;
+	stats->alloc_fail = total_stats.rx_alloc_fail;
+}
+
+static const struct netdev_stat_ops rmnet_stat_ops = {
+	.get_queue_stats_rx = rmnet_get_queue_stats_rx,
+};
+
 static const struct net_device_ops rmnet_vnd_ops = {
 	.ndo_start_xmit = rmnet_vnd_start_xmit,
 	.ndo_change_mtu = rmnet_vnd_change_mtu,
@@ -192,8 +235,34 @@ static const char rmnet_gstrings_stats[][ETH_GSTRING_LEN] = {
 	"Checksum skipped",
 	"Checksum computed in software",
 	"Checksum computed in hardware",
+	/* DL coalescing */
+	"Coal alloc packet drops",
+	"Coal frames received",
+	"Packets in coal frames",
+	"Coal header errors",
+	"Coal hdr pkt count errors",
+	"Coal bounds errors",
+	"Coal checksum errors",
+	"Coal packets dropped on csum err",
+	"Coal segments reconstructed",
+	"Coal invalid IP packet",
+	"Coal invalid transport",
+	/* close reasons */
+	"Coal closed: non-coal",
+	"Coal closed: IP miss",
+	"Coal closed: transport miss",
+	"Coal closed: hw NL limit",
+	"Coal closed: hw pkt limit",
+	"Coal closed: hw byte limit",
+	"Coal closed: hw time limit",
+	"Coal closed: hw evict",
+	"Coal closed: FIN/PSH",
 };
 
+static_assert(ARRAY_SIZE(rmnet_gstrings_stats) ==
+	      (sizeof(struct rmnet_priv_stats) -
+	       offsetof(struct rmnet_priv_stats, csum_ok)) / sizeof(u64));
+
 static void rmnet_get_strings(struct net_device *dev, u32 stringset, u8 *buf)
 {
 	switch (stringset) {
@@ -218,12 +287,62 @@ static void rmnet_get_ethtool_stats(struct net_device *dev,
 				    struct ethtool_stats *stats, u64 *data)
 {
 	struct rmnet_priv *priv = netdev_priv(dev);
-	struct rmnet_priv_stats *st = &priv->stats;
+	struct rmnet_priv_stats total_stats = { };
+	struct rmnet_pcpu_stats *pcpu_ptr;
+	struct rmnet_priv_stats snapshot;
+	unsigned int cpu, start;
 
 	if (!data)
 		return;
 
-	memcpy(data, st, ARRAY_SIZE(rmnet_gstrings_stats) * sizeof(u64));
+	for_each_possible_cpu(cpu) {
+		pcpu_ptr = per_cpu_ptr(priv->pcpu_stats, cpu);
+
+		do {
+			start = u64_stats_fetch_begin(&pcpu_ptr->syncp);
+			u64_stats_copy(&snapshot, &pcpu_ptr->priv_stats,
+				       sizeof(snapshot));
+		} while (u64_stats_fetch_retry(&pcpu_ptr->syncp, start));
+
+		total_stats.csum_ok += snapshot.csum_ok;
+		total_stats.csum_ip4_header_bad += snapshot.csum_ip4_header_bad;
+		total_stats.csum_valid_unset += snapshot.csum_valid_unset;
+		total_stats.csum_validation_failed += snapshot.csum_validation_failed;
+		total_stats.csum_err_bad_buffer += snapshot.csum_err_bad_buffer;
+		total_stats.csum_err_invalid_ip_version +=
+			snapshot.csum_err_invalid_ip_version;
+		total_stats.csum_err_invalid_transport +=
+			snapshot.csum_err_invalid_transport;
+		total_stats.csum_fragmented_pkt += snapshot.csum_fragmented_pkt;
+		total_stats.csum_skipped += snapshot.csum_skipped;
+		total_stats.csum_sw += snapshot.csum_sw;
+		total_stats.csum_hw += snapshot.csum_hw;
+		/* Custom rmnet coalescing diagnostics follow. */
+		total_stats.coal_alloc_fail += snapshot.coal_alloc_fail;
+		total_stats.coal_rx += snapshot.coal_rx;
+		total_stats.coal_pkts += snapshot.coal_pkts;
+		total_stats.coal_hdr_err += snapshot.coal_hdr_err;
+		total_stats.coal_hdr_pkt_err += snapshot.coal_hdr_pkt_err;
+		total_stats.coal_bounds_err += snapshot.coal_bounds_err;
+		total_stats.coal_csum_err += snapshot.coal_csum_err;
+		total_stats.coal_csum_drop += snapshot.coal_csum_drop;
+		total_stats.coal_reconstruct += snapshot.coal_reconstruct;
+		total_stats.coal_ip_invalid += snapshot.coal_ip_invalid;
+		total_stats.coal_trans_invalid += snapshot.coal_trans_invalid;
+		total_stats.coal_close_non_coal += snapshot.coal_close_non_coal;
+		total_stats.coal_close_ip_miss += snapshot.coal_close_ip_miss;
+		total_stats.coal_close_trans_miss += snapshot.coal_close_trans_miss;
+		total_stats.coal_close_hw_nl += snapshot.coal_close_hw_nl;
+		total_stats.coal_close_hw_pkt += snapshot.coal_close_hw_pkt;
+		total_stats.coal_close_hw_byte += snapshot.coal_close_hw_byte;
+		total_stats.coal_close_hw_time += snapshot.coal_close_hw_time;
+		total_stats.coal_close_hw_evict += snapshot.coal_close_hw_evict;
+		total_stats.coal_close_coal += snapshot.coal_close_coal;
+	}
+
+	/* The first four fields are reported through netdev queue statistics. */
+	memcpy(data, &total_stats.csum_ok,
+	       sizeof(total_stats) - offsetof(struct rmnet_priv_stats, csum_ok));
 }
 
 static int rmnet_get_coalesce(struct net_device *dev,
@@ -332,6 +451,7 @@ int rmnet_vnd_newlink(u8 id, struct net_device *rmnet_dev,
 	rmnet_dev->hw_features |= NETIF_F_IP_CSUM | NETIF_F_IPV6_CSUM;
 	rmnet_dev->hw_features |= NETIF_F_SG;
 	rmnet_dev->hw_features |= NETIF_F_GRO_HW;
+	rmnet_dev->stat_ops = &rmnet_stat_ops;
 
 	priv->real_dev = real_dev;
 
-- 
2.34.1


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

* [PATCH net-next v2 8/8] docs: networking: Add documentation for the coalescing support in rmnet
  2026-10-08  0:55 [PATCH net-next v2 0/8] Add HW GRO handling in rmnet Subash Abhinov Kasiviswanathan
                   ` (6 preceding siblings ...)
  2026-10-08  0:55 ` [PATCH net-next v2 7/8] net: qualcomm: rmnet: Add DL coalescing statistics Subash Abhinov Kasiviswanathan
@ 2026-10-08  0:55 ` Subash Abhinov Kasiviswanathan
  7 siblings, 0 replies; 10+ messages in thread
From: Subash Abhinov Kasiviswanathan @ 2026-10-08  0:55 UTC (permalink / raw)
  To: davem, edumazet, kuba, pabeni, andrew+netdev, corbet
  Cc: horms, skhan, rdunlap, netdev, linux-doc, linux-kernel,
	Subash Abhinov Kasiviswanathan, Sean Tranchetti

Add information about the MAPv5 coalescing header covering the layout
and the information from the fields in the header.

Document the IFLA_RMNET_FLAGS coalescing rules. Ingress coalescing
requires MAPv5 checksum offload, and MAPv4 and MAPv5 checksum
configurations cannot be enabled together in either direction. A direction
may leave checksum offload disabled.

Document that the frame-level CSUM valid indication is distinct from the
per-packet CSUM error bitmap. Multi-packet coalesced frames require RX
checksum offload and rx-gro-hw. Single-packet frames are delivered as
normal non-GSO skbs when those features are disabled.

Co-developed-by: Sean Tranchetti <sean.tranchetti@oss.qualcomm.com>
Signed-off-by: Sean Tranchetti <sean.tranchetti@oss.qualcomm.com>
Signed-off-by: Subash Abhinov Kasiviswanathan <subash.a.kasiviswanathan@oss.qualcomm.com>
---
v2:
  - Update the documentation and commit text to clarify the hardware behavior
v1: https://lore.kernel.org/all/20260930051345.857443-8-subash.a.kasiviswanathan@oss.qualcomm.com/

 .../cellular/qualcomm/rmnet.rst               | 153 +++++++++++++++++-
 1 file changed, 145 insertions(+), 8 deletions(-)

diff --git a/Documentation/networking/device_drivers/cellular/qualcomm/rmnet.rst b/Documentation/networking/device_drivers/cellular/qualcomm/rmnet.rst
index 5aedbabb7382..52f92d2fba31 100644
--- a/Documentation/networking/device_drivers/cellular/qualcomm/rmnet.rst
+++ b/Documentation/networking/device_drivers/cellular/qualcomm/rmnet.rst
@@ -125,8 +125,8 @@ Command (1)/ Data (0) bit value is to indicate if the packet is a MAP command
 or data packet. Command packet is used for transport level flow control. Data
 packets are standard IP packets.
 
-Next header is used to indicate the presence of another header, currently is
-limited to checksum header.
+Next header is used to indicate the presence of another header, currently
+limited to the checksum and coalescing headers.
 
 Padding is the number of bytes to be appended to the payload to
 ensure 4 byte alignment.
@@ -150,11 +150,11 @@ Header Type is to indicate the type of header, this usually is set to CHECKSUM
 
 Header types
 
-= ===============
+= ======================
 0 Reserved
-1 Reserved
+1 coalescing header
 2 checksum header
-= ===============
+= ======================
 
 Checksum Valid is to indicate whether the header checksum is valid. Value of 1
 implies that checksum is calculated on this packet and is valid, value of 0
@@ -162,8 +162,109 @@ indicates that the calculated packet checksum is invalid.
 
 Reserved bits must be zero when sent and ignored when received.
 
-e. MAP packet v1/v5 (command specific)
---------------------------------------
+e. Coalescing header v5
+------------------------
+
+Hardware can coalesce multiple same-flow IP packets into a single MAP frame
+to reduce per-packet overhead at high data rates.  Packets are grouped into
+NLOs, with all packets in each NLO having the same length.  The coalescing
+header (header type 1) describes the coalesced content.
+
+Packet format::
+
+  Bit         0 - 6         7          8           9-11         12-15
+  Function  Header Type  Next Header  CSUM valid  Num NLOs    (reserved)
+
+  Bit        16-19        20-23
+  Function  Close value  Close type
+
+  Bit        24-27        28-31
+  Function  (reserved)    VEID
+
+  Bit           32 - 47        48 - 55            56 - 63
+  Function   Packet length  CSUM error bitmap  Num packets (NLO 0)
+
+  ... (up to 6 NLO entries total, same 32-bit format per entry)
+
+Header Type is set to 1 (coalescing).
+
+The MAP header ``pkt_len`` includes the coalescing header, coalesced packet
+data, and any MAP padding.
+
+Num NLOs (Number-Length Objects) is the count of active NLO entries
+(1 – 6).  Each NLO describes a group of consecutive coalesced packets
+that all share the same IP packet length.
+
+Num NLOs identifies the active prefix of the six NLO slots.  The header
+always contains all six slots and remains 28 bytes long regardless of Num
+NLOs.  A slot with ``num_packets == 0`` ends the active NLO prefix.  The
+full coalescing header is included in the MAP header's ``pkt_len``.
+
+Some hardware may report Num NLOs incorrectly.  For compatibility, receivers
+should derive the active prefix from ``num_packets`` when needed.  This
+requires unused slots to have ``num_packets == 0``.
+
+CSUM valid (bit 8) is a frame-level indication that the hardware checksum is
+valid for all packets in the frame. It is distinct from the per-packet CSUM
+error bitmap in each NLO entry.
+
+For a single-NLO, single-packet frame, CSUM valid must not be trusted when
+the close reason is a FIN/PSH close, a packet limit, a byte limit, or a time
+limit.  In these cases, the rmnet driver treats the packet checksum as
+unverified and lets the network stack validate it instead of relying on the
+coalescing checksum indications.
+IPv4 UDP packets with a zero checksum remain valid because that checksum is
+optional.
+
+Close type and close value encode the hardware reason that coalescing
+was terminated for this frame:
+
+Close type values:
+
+= ==============================
+0 non-coalesced (single packet)
+1 IP flow miss
+2 transport flow miss
+3 hardware limit (see value)
+4 coalescing closed (FIN/PSH)
+= ==============================
+
+Close value (used when close type is 3):
+
+= ==================
+0 NL limit reached
+1 packet limit
+2 byte limit
+3 time limit
+4 eviction
+= ==================
+
+VEID is the virtual endpoint ID of the originating flow.
+
+Each NLO entry::
+
+  Bit         0 - 15        16 - 23            24 - 31
+  Function  Pkt length   CSUM error bitmap   Num packets
+
+Pkt length is the full IP packet length, including the IP header, transport
+header, and payload, for every packet in this NLO group. The IP and transport
+headers are present once in the coalesced frame and are not repeated for each
+packet.
+
+CSUM error bitmap is one 48-bit stream formed by concatenating the
+``csum_error_bitmap`` bytes from all six NLO slots in slot order. Packet 0
+corresponds to bit 0 of slot 0's bitmap byte, and bits are consumed from
+least significant bit to most significant bit. The bit stream is indexed by
+the packet's absolute position in the frame and does not restart at an NLO
+boundary. If an NLO contains more than eight packets, its error bits
+continue into the bitmap byte of the following slot. Bitmap bytes in slots
+after the active NLO prefix may therefore contain continuation bits and must
+not be ignored.
+
+Num packets is the count of coalesced packets described by this NLO.
+
+f. MAP packet v1/v5 (command specific)
+---------------------------------------
 
 Packet format::
 
@@ -187,7 +288,7 @@ Command types
 3 is for error during processing of commands
 = ==========================================
 
-f. Aggregation
+g. Aggregation
 --------------
 
 Aggregation is multiple MAP packets (can be data or command) delivered to
@@ -208,3 +309,39 @@ rmnet userspace configuration is done through netlink using iproute2
 https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/
 
 The driver uses rtnl_link_ops for communication.
+
+The data format flags controlling the ingress and egress processing
+pipeline are set via the ``IFLA_RMNET_FLAGS`` attribute
+(``struct ifla_rmnet_flags``).
+
+Relevant ingress flags:
+
+``RMNET_FLAGS_INGRESS_DEAGGREGATION``
+  Enable MAP frame de-aggregation.
+
+``RMNET_FLAGS_INGRESS_MAP_CKSUMV4``
+  Enable MAPv4 downlink checksum offload.
+
+``RMNET_FLAGS_INGRESS_MAP_CKSUMV5``
+  Enable MAPv5 downlink checksum offload (header type 2).
+
+``RMNET_FLAGS_INGRESS_COALESCE``
+  Enable MAPv5 downlink hardware coalescing (header type 1).
+  This flag requires ``RMNET_FLAGS_INGRESS_MAP_CKSUMV5``. MAPv4 and
+  MAPv5 checksum flags cannot be enabled together in either direction.
+  A direction may leave checksum offload disabled. Invalid combinations
+  are rejected.
+  When RX checksum offload and ``rx-gro-hw`` are enabled, valid
+  multi-packet coalesced frames can be delivered as batched GSO SKBs.
+  Otherwise, multi-packet coalesced frames are rejected. A coalesced frame
+  containing one packet is delivered as a normal non-GSO skb. IP options,
+  IPv6 extension headers, and zero-payload packets can also prevent GSO
+  processing for a frame.
+
+Relevant egress flags:
+
+``RMNET_FLAGS_EGRESS_MAP_CKSUMV4``
+  Enable MAPv4 uplink checksum offload.
+
+``RMNET_FLAGS_EGRESS_MAP_CKSUMV5``
+  Enable MAPv5 uplink checksum offload.
-- 
2.34.1


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

* Re: [PATCH net-next v2 7/8] net: qualcomm: rmnet: Add DL coalescing statistics
  2026-10-08  0:55 ` [PATCH net-next v2 7/8] net: qualcomm: rmnet: Add DL coalescing statistics Subash Abhinov Kasiviswanathan
@ 2026-10-08 23:59   ` kernel test robot
  0 siblings, 0 replies; 10+ messages in thread
From: kernel test robot @ 2026-10-08 23:59 UTC (permalink / raw)
  To: Subash Abhinov Kasiviswanathan, davem, edumazet, kuba, pabeni,
	andrew+netdev, corbet
  Cc: llvm, oe-kbuild-all, horms, skhan, rdunlap, netdev, linux-doc,
	linux-kernel, Subash Abhinov Kasiviswanathan, Sean Tranchetti

Hi Subash,

kernel test robot noticed the following build warnings:

[auto build test WARNING on net-next/main]
[also build test WARNING on next-20261008]
[cannot apply to linux-review/Subash-Abhinov-Kasiviswanathan/uapi-if_link-Add-RMNET_FLAGS_INGRESS_COALESCE/20260929-221339 net/main lwn/docs-next linus/master v7.3-rc6]
[If your patch is applied to the wrong git tree, kindly drop us a note.
And when submitting patch, we suggest to use '--base' as documented in
https://git-scm.com/docs/git-format-patch#_base_tree_information]

url:    https://github.com/intel-lab-lkp/linux/commits/Subash-Abhinov-Kasiviswanathan/net-qualcomm-rmnet-Update-MTU-handling-during-format-changes/20261007-175538
base:   net-next/main
patch link:    https://lore.kernel.org/r/20261008005543.2630828-8-subash.a.kasiviswanathan%40oss.qualcomm.com
patch subject: [PATCH net-next v2 7/8] net: qualcomm: rmnet: Add DL coalescing statistics
config: hexagon-allmodconfig (https://download.01.org/0day-ci/archive/20261009/202610090707.TY5x2Oyd-lkp@intel.com/config)
compiler: clang version 24.0.0git (https://github.com/llvm/llvm-project 242243a5814307de825db3428662d67b064e2cbc)
reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20261009/202610090707.TY5x2Oyd-lkp@intel.com/reproduce)

If you fix the issue in a separate patch/commit (i.e. not just a new version of
the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <lkp@intel.com>
| Closes: https://lore.kernel.org/oe-kbuild-all/202610090707.TY5x2Oyd-lkp@intel.com/

All warnings (new ones prefixed by >>):

>> drivers/net/ethernet/qualcomm/rmnet/rmnet_vnd.c:286:13: warning: stack frame size (1392) exceeds limit (1280) in 'rmnet_get_ethtool_stats' [-Wframe-larger-than]
     286 | static void rmnet_get_ethtool_stats(struct net_device *dev,
         |             ^
   1 warning generated.


vim +/rmnet_get_ethtool_stats +286 drivers/net/ethernet/qualcomm/rmnet/rmnet_vnd.c

bbde32d38bfbbc4 Subash Abhinov Kasiviswanathan 2018-05-15  285  
bbde32d38bfbbc4 Subash Abhinov Kasiviswanathan 2018-05-15 @286  static void rmnet_get_ethtool_stats(struct net_device *dev,
bbde32d38bfbbc4 Subash Abhinov Kasiviswanathan 2018-05-15  287  				    struct ethtool_stats *stats, u64 *data)
bbde32d38bfbbc4 Subash Abhinov Kasiviswanathan 2018-05-15  288  {
bbde32d38bfbbc4 Subash Abhinov Kasiviswanathan 2018-05-15  289  	struct rmnet_priv *priv = netdev_priv(dev);
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  290  	struct rmnet_priv_stats total_stats = { };
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  291  	struct rmnet_pcpu_stats *pcpu_ptr;
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  292  	struct rmnet_priv_stats snapshot;
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  293  	unsigned int cpu, start;
bbde32d38bfbbc4 Subash Abhinov Kasiviswanathan 2018-05-15  294  
bbde32d38bfbbc4 Subash Abhinov Kasiviswanathan 2018-05-15  295  	if (!data)
bbde32d38bfbbc4 Subash Abhinov Kasiviswanathan 2018-05-15  296  		return;
bbde32d38bfbbc4 Subash Abhinov Kasiviswanathan 2018-05-15  297  
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  298  	for_each_possible_cpu(cpu) {
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  299  		pcpu_ptr = per_cpu_ptr(priv->pcpu_stats, cpu);
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  300  
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  301  		do {
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  302  			start = u64_stats_fetch_begin(&pcpu_ptr->syncp);
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  303  			u64_stats_copy(&snapshot, &pcpu_ptr->priv_stats,
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  304  				       sizeof(snapshot));
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  305  		} while (u64_stats_fetch_retry(&pcpu_ptr->syncp, start));
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  306  
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  307  		total_stats.csum_ok += snapshot.csum_ok;
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  308  		total_stats.csum_ip4_header_bad += snapshot.csum_ip4_header_bad;
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  309  		total_stats.csum_valid_unset += snapshot.csum_valid_unset;
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  310  		total_stats.csum_validation_failed += snapshot.csum_validation_failed;
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  311  		total_stats.csum_err_bad_buffer += snapshot.csum_err_bad_buffer;
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  312  		total_stats.csum_err_invalid_ip_version +=
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  313  			snapshot.csum_err_invalid_ip_version;
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  314  		total_stats.csum_err_invalid_transport +=
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  315  			snapshot.csum_err_invalid_transport;
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  316  		total_stats.csum_fragmented_pkt += snapshot.csum_fragmented_pkt;
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  317  		total_stats.csum_skipped += snapshot.csum_skipped;
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  318  		total_stats.csum_sw += snapshot.csum_sw;
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  319  		total_stats.csum_hw += snapshot.csum_hw;
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  320  		/* Custom rmnet coalescing diagnostics follow. */
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  321  		total_stats.coal_alloc_fail += snapshot.coal_alloc_fail;
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  322  		total_stats.coal_rx += snapshot.coal_rx;
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  323  		total_stats.coal_pkts += snapshot.coal_pkts;
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  324  		total_stats.coal_hdr_err += snapshot.coal_hdr_err;
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  325  		total_stats.coal_hdr_pkt_err += snapshot.coal_hdr_pkt_err;
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  326  		total_stats.coal_bounds_err += snapshot.coal_bounds_err;
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  327  		total_stats.coal_csum_err += snapshot.coal_csum_err;
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  328  		total_stats.coal_csum_drop += snapshot.coal_csum_drop;
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  329  		total_stats.coal_reconstruct += snapshot.coal_reconstruct;
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  330  		total_stats.coal_ip_invalid += snapshot.coal_ip_invalid;
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  331  		total_stats.coal_trans_invalid += snapshot.coal_trans_invalid;
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  332  		total_stats.coal_close_non_coal += snapshot.coal_close_non_coal;
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  333  		total_stats.coal_close_ip_miss += snapshot.coal_close_ip_miss;
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  334  		total_stats.coal_close_trans_miss += snapshot.coal_close_trans_miss;
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  335  		total_stats.coal_close_hw_nl += snapshot.coal_close_hw_nl;
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  336  		total_stats.coal_close_hw_pkt += snapshot.coal_close_hw_pkt;
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  337  		total_stats.coal_close_hw_byte += snapshot.coal_close_hw_byte;
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  338  		total_stats.coal_close_hw_time += snapshot.coal_close_hw_time;
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  339  		total_stats.coal_close_hw_evict += snapshot.coal_close_hw_evict;
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  340  		total_stats.coal_close_coal += snapshot.coal_close_coal;
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  341  	}
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  342  
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  343  	/* The first four fields are reported through netdev queue statistics. */
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  344  	memcpy(data, &total_stats.csum_ok,
2a458504868286e Subash Abhinov Kasiviswanathan 2026-10-07  345  	       sizeof(total_stats) - offsetof(struct rmnet_priv_stats, csum_ok));
bbde32d38bfbbc4 Subash Abhinov Kasiviswanathan 2018-05-15  346  }
bbde32d38bfbbc4 Subash Abhinov Kasiviswanathan 2018-05-15  347  

--
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki

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

end of thread, other threads:[~2026-10-08 23:59 UTC | newest]

Thread overview: 10+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-08  0:55 [PATCH net-next v2 0/8] Add HW GRO handling in rmnet Subash Abhinov Kasiviswanathan
2026-10-08  0:55 ` [PATCH net-next v2 1/8] net: qualcomm: rmnet: Update MTU handling during format changes Subash Abhinov Kasiviswanathan
2026-10-08  0:55 ` [PATCH net-next v2 2/8] uapi: if_link: Add RMNET_FLAGS_INGRESS_COALESCE Subash Abhinov Kasiviswanathan
2026-10-08  0:55 ` [PATCH net-next v2 3/8] net: qualcomm: rmnet: Process MAPv5 frames as a list Subash Abhinov Kasiviswanathan
2026-10-08  0:55 ` [PATCH net-next v2 4/8] net: qualcomm: rmnet: Restrict supported MAP checksum configurations Subash Abhinov Kasiviswanathan
2026-10-08  0:55 ` [PATCH net-next v2 5/8] net: qualcomm: rmnet: Add DL packet coalescing support Subash Abhinov Kasiviswanathan
2026-10-08  0:55 ` [PATCH net-next v2 6/8] net: qualcomm: rmnet: Work around coalescing hardware quirks Subash Abhinov Kasiviswanathan
2026-10-08  0:55 ` [PATCH net-next v2 7/8] net: qualcomm: rmnet: Add DL coalescing statistics Subash Abhinov Kasiviswanathan
2026-10-08 23:59   ` kernel test robot
2026-10-08  0:55 ` [PATCH net-next v2 8/8] docs: networking: Add documentation for the coalescing support in rmnet Subash Abhinov Kasiviswanathan

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox