[PATCH net v4] net: qualcomm: rmnet: Update MTU handling during format changes

From
Subash Abhinov Kasiviswanathan <subash.a.kasiviswanathan@oss.qualcomm.com>
Date
2026-11-08 07:59:19
Also in
lkml
rmnet derives the MTU of its devices from the real device MTU minus the
headroom needed by the egress data format. Several problems exist in
how this is handled when the data format is set or changed:

- The headroom only accounts for the V4 egress checksum header and
  ignores the V5 one, so devices using V5 checksum offload get an MTU
  that is too large for the real device.
- rmnet_newlink() sizes the first device from the port's data format
  before the requested IFLA_RMNET_FLAGS have been applied, so the MTU
  does not match the format that ends up being used.
- A real device MTU smaller than the headroom underflows the unsigned
  subtraction, and a real device with a jumbo MTU makes the creation of
  the first device fail while resizing an existing one clamps it.
- Existing endpoints are shrunk by writing dev->mtu directly. No MTU
  notifiers run and userspace gets no link notification. The helper
  also returns -1, and changelink flattens every failure into -EINVAL.
- rmnet_changelink() publishes the new data format before resizing the
  endpoints and reverts it on failure, so the datapath can observe a
  format whose headers do not fit the MTUs. A failed resize can also
  leave some endpoints shrunk, and a mux_id change requested in the
  same call is kept even though the call returned an error.

Fix these by calculating the headroom from the requested data format,
including the V5 checksum header, and passing that format through the
MTU helpers so that new and existing devices use the same calculation.
Apply IFLA_RMNET_FLAGS before sizing the first device, reject real
device MTUs below the headroom before doing any arithmetic, and clamp
the target to RMNET_MAX_PACKET_SIZE for both new and existing devices.

Resize existing endpoints with dev_set_mtu() so that the normal MTU
notifiers and userspace link notifications are delivered. If resizing
an endpoint fails, restore the endpoints which were already resized and
ignore any errors from the restore, and return the error to the caller
with a message that is not specific to the real device MTU.

Publish the new shared data format only after all MTUs have been
updated, and in rmnet_changelink() move the endpoint to a new mux_id
only after the checks and the MTU update have succeeded, so that a
failed request does not leave a partial change behind.

Fixes: b7f5eb6ba21b ("net: qualcomm: rmnet: Update rmnet device MTU based on real device")
Fixes: b6e5d27e32ef ("net: ethernet: rmnet: Add support for MAPv5 egress packets")
Assisted-by: LLM
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>
---
v4: Separated patch from the coalescing series into a standalone fix for
mtu behavior. This addresses the comments raised by Sashiko to honor the
room required for MAPv5 format, better aritmetic handling and more
checks for cases where the real device MTU exceeds RMNET_MAX_PACKET_SIZE.
v3: https://lore.kernel.org/all/20261009084351.317632-2-subash.a.kasiviswanathan@oss.qualcomm.com/ (local)
v2: https://lore.kernel.org/all/20261008005543.2630828-2-subash.a.kasiviswanathan@oss.qualcomm.com/ (local)

 .../ethernet/qualcomm/rmnet/rmnet_config.c    |  77 +++++++-----
 .../ethernet/qualcomm/rmnet/rmnet_config.h    |   1 +
 .../net/ethernet/qualcomm/rmnet/rmnet_vnd.c   | 110 +++++++++++++-----
 .../net/ethernet/qualcomm/rmnet/rmnet_vnd.h   |   4 +-
 4 files changed, 133 insertions(+), 59 deletions(-)
diff --git a/drivers/net/ethernet/qualcomm/rmnet/rmnet_config.c b/drivers/net/ethernet/qualcomm/rmnet/rmnet_config.c
index 61b04c6c0390..9e66f3804de4 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,25 @@ 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) {
+		NL_SET_ERR_MSG_MOD(extack, "Failed to update MTU of rmnet devices");
+		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 +311,14 @@ static int rmnet_changelink(struct net_device *dev, struct nlattr *tb[],
 			    struct netlink_ext_ack *extack)
 {
 	struct rmnet_priv *priv = netdev_priv(dev);
+	struct rmnet_endpoint *ep = NULL;
+	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,12 +335,17 @@ 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]);
 
 		if (mux_id != priv->mux_id) {
-			struct rmnet_endpoint *ep;
-
 			ep = rmnet_get_endpoint(port, priv->mux_id);
 			if (!ep)
 				return -ENODEV;
@@ -335,32 +355,29 @@ static int rmnet_changelink(struct net_device *dev, struct nlattr *tb[],
 						   "MUX ID already exists");
 				return -EINVAL;
 			}
-
-			hlist_del_init_rcu(&ep->hlnode);
-			WRITE_ONCE(ep->mux_id, mux_id);
-			hlist_add_head_rcu(&ep->hlnode,
-					   &port->muxed_ep[mux_id]);
-
-			WRITE_ONCE(priv->mux_id, mux_id);
 		}
 	}
 
 	if (data[IFLA_RMNET_FLAGS]) {
-		struct ifla_rmnet_flags *flags;
-		u32 old_data_format;
-		u32 data_format;
+		err = rmnet_vnd_update_dev_mtu(port, real_dev, data_format);
+		if (err) {
+			NL_SET_ERR_MSG_MOD(extack,
+					   "Failed to update MTU of rmnet devices");
+			return err;
+		}
 
-		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);
-			NL_SET_ERR_MSG_MOD(extack, "Invalid MTU on real dev");
-			return -EINVAL;
-		}
+	/* Nothing below can fail, so move the endpoint only after all the
+	 * checks and the MTU update have succeeded.
+	 */
+	if (ep) {
+		hlist_del_init_rcu(&ep->hlnode);
+		WRITE_ONCE(ep->mux_id, mux_id);
+		hlist_add_head_rcu(&ep->hlnode, &port->muxed_ep[mux_id]);
+
+		WRITE_ONCE(priv->mux_id, mux_id);
 	}
 
 	return 0;
diff --git a/drivers/net/ethernet/qualcomm/rmnet/rmnet_config.h b/drivers/net/ethernet/qualcomm/rmnet/rmnet_config.h
index 5adda0323dda..47b8db3206b9 100644
--- a/drivers/net/ethernet/qualcomm/rmnet/rmnet_config.h
+++ b/drivers/net/ethernet/qualcomm/rmnet/rmnet_config.h
@@ -19,6 +19,7 @@ struct rmnet_endpoint {
 	struct net_device *egress_dev;
 	struct hlist_node hlnode;
 	struct rcu_head rcu;
+	int saved_mtu;
 };
 
 struct rmnet_egress_agg_params {
diff --git a/drivers/net/ethernet/qualcomm/rmnet/rmnet_vnd.c b/drivers/net/ethernet/qualcomm/rmnet/rmnet_vnd.c
index 5f921cddf82b..dce436e4c630 100644
--- a/drivers/net/ethernet/qualcomm/rmnet/rmnet_vnd.c
+++ b/drivers/net/ethernet/qualcomm/rmnet/rmnet_vnd.c
@@ -66,29 +66,44 @@ 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);
+	else if (data_format & RMNET_FLAGS_EGRESS_MAP_CKSUMV5)
+		headroom += sizeof(struct rmnet_map_v5_csum_header);
 
 	return headroom;
 }
 
-static int rmnet_vnd_change_mtu(struct net_device *rmnet_dev, int new_mtu)
+/* Largest MTU an rmnet device can use on top of real_dev for the given data
+ * format, or -EINVAL if real_dev cannot even fit the headers.
+ */
+static int rmnet_vnd_target_mtu(struct net_device *real_dev, 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(data_format);
+	if (real_dev->mtu < headroom)
+		return -EINVAL;
+
+	return min_t(int, real_dev->mtu - headroom, RMNET_MAX_PACKET_SIZE);
+}
+
+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);
+	u32 headroom;
 
-	headroom = rmnet_vnd_headroom(port);
+	headroom = rmnet_vnd_headroom(data_format);
 
-	if (new_mtu < 0 || new_mtu > RMNET_MAX_PACKET_SIZE ||
+	if (priv->real_dev->mtu < headroom ||
+	    new_mtu < 0 || new_mtu > RMNET_MAX_PACKET_SIZE ||
 	    new_mtu > (priv->real_dev->mtu - headroom))
 		return -EINVAL;
 
@@ -96,6 +111,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,11 +334,10 @@ 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;
+	int new_mtu;
 	int rc;
 
 	if (rmnet_get_endpoint(port, id)) {
@@ -326,9 +351,9 @@ int rmnet_vnd_newlink(u8 id, struct net_device *rmnet_dev,
 
 	priv->real_dev = real_dev;
 
-	headroom = rmnet_vnd_headroom(port);
-
-	if (rmnet_vnd_change_mtu(rmnet_dev, real_dev->mtu - headroom)) {
+	new_mtu = rmnet_vnd_target_mtu(real_dev, data_format);
+	if (new_mtu < 0 ||
+	    rmnet_vnd_change_mtu_with_format(rmnet_dev, new_mtu, data_format)) {
 		NL_SET_ERR_MSG_MOD(extack, "Invalid MTU on real dev");
 		return -EINVAL;
 	}
@@ -375,17 +400,17 @@ 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);
+	if (real_dev->mtu < headroom)
+		return -EINVAL;
 
 	hash_for_each_safe(port->muxed_ep, bkt_ep, tmp_ep, ep, hlnode) {
 		if (ep->egress_dev->mtu > (real_dev->mtu - headroom))
@@ -395,24 +420,55 @@ 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 rmnet_endpoint *fail_ep = NULL;
 	struct hlist_node *tmp_ep;
 	struct rmnet_endpoint *ep;
 	unsigned long bkt_ep;
-	u32 headroom;
+	int new_mtu;
+	int err = 0;
 
-	headroom = rmnet_vnd_headroom(port);
+	new_mtu = rmnet_vnd_target_mtu(real_dev, data_format);
+	if (new_mtu < 0)
+		return new_mtu;
 
 	hash_for_each_safe(port->muxed_ep, bkt_ep, tmp_ep, ep, hlnode) {
-		if (ep->egress_dev->mtu <= (real_dev->mtu - headroom))
+		ep->saved_mtu = ep->egress_dev->mtu;
+		if (ep->saved_mtu <= new_mtu)
 			continue;
 
-		if (rmnet_vnd_change_mtu(ep->egress_dev,
-					 real_dev->mtu - headroom))
-			return -1;
+		err = dev_set_mtu(ep->egress_dev, new_mtu);
+		if (err) {
+			fail_ep = ep;
+			break;
+		}
 	}
 
-	return 0;
+	if (!err)
+		return 0;
+
+	/* Restore the endpoints which were already resized. Failures here
+	 * are ignored since the original error is the one to report.
+	 */
+	hash_for_each_safe(port->muxed_ep, bkt_ep, tmp_ep, ep, hlnode) {
+		if (ep == fail_ep)
+			break;
+
+		if (ep->egress_dev->mtu != ep->saved_mtu)
+			dev_set_mtu(ep->egress_dev, ep->saved_mtu);
+	}
+
+	return err;
 }
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
Thread (3 messages) 3 messages, 2 authors, last reply 5h ago
Keyboard shortcuts
hback out one level
jnext message in thread / next series-diff position
kprevious message in thread / previous series-diff position
ldrill in
Escclose help / fold thread tree
?toggle this help