ethtool: correctly ensure {GS}CHANNELS doesn't conflict with GS{RXFH}
authorKeller, Jacob E <jacob.e.keller@intel.com>
Tue, 9 Feb 2016 00:05:03 +0000 (16:05 -0800)
committerDavid S. Miller <davem@davemloft.net>
Tue, 16 Feb 2016 20:19:49 +0000 (15:19 -0500)
Ethernet drivers implementing both {GS}RXFH and {GS}CHANNELS ethtool ops
incorrectly allow SCHANNELS when it would conflict with the settings
from SRXFH. This occurs because it is not possible for drivers to
understand whether their Rx flow indirection table has been configured
or is in the default state. In addition, drivers currently behave in
various ways when increasing the number of Rx channels.

Some drivers will always destroy the Rx flow indirection table when this
occurs, whether it has been set by the user or not. Other drivers will
attempt to preserve the table even if the user has never modified it
from the default driver settings. Neither of these situation is
desirable because it leads to unexpected behavior or loss of user
configuration.

The correct behavior is to simply return -EINVAL when SCHANNELS would
conflict with the current Rx flow table settings. However, it should
only do so if the current settings were modified by the user. If we
required that the new settings never conflict with the current (default)
Rx flow settings, we would force users to first reduce their Rx flow
settings and then reduce the number of Rx channels.

This patch proposes a solution implemented in net/core/ethtool.c which
ensures that all drivers behave correctly. It checks whether the RXFH
table has been configured to non-default settings, and stores this
information in a private netdev flag. When the number of channels is
requested to change, it first ensures that the current Rx flow table is
not going to assign flows to now disabled channels.

Signed-off-by: Jacob Keller <jacob.e.keller@intel.com>
Signed-off-by: David S. Miller <davem@davemloft.net>
include/linux/netdevice.h
net/core/ethtool.c

index 219f53c30cb3cd4a9a2fdd1c2a2d3ca343f28bca..0499569c256d09aa16152a99b128bfa59c2c27d9 100644 (file)
@@ -1291,6 +1291,7 @@ struct net_device_ops {
  * @IFF_OPENVSWITCH: device is a Open vSwitch master
  * @IFF_L3MDEV_SLAVE: device is enslaved to an L3 master device
  * @IFF_TEAM: device is a team device
+ * @IFF_RXFH_CONFIGURED: device has had Rx Flow indirection table configured
  */
 enum netdev_priv_flags {
        IFF_802_1Q_VLAN                 = 1<<0,
@@ -1318,6 +1319,7 @@ enum netdev_priv_flags {
        IFF_OPENVSWITCH                 = 1<<22,
        IFF_L3MDEV_SLAVE                = 1<<23,
        IFF_TEAM                        = 1<<24,
+       IFF_RXFH_CONFIGURED             = 1<<25,
 };
 
 #define IFF_802_1Q_VLAN                        IFF_802_1Q_VLAN
@@ -1345,6 +1347,7 @@ enum netdev_priv_flags {
 #define IFF_OPENVSWITCH                        IFF_OPENVSWITCH
 #define IFF_L3MDEV_SLAVE               IFF_L3MDEV_SLAVE
 #define IFF_TEAM                       IFF_TEAM
+#define IFF_RXFH_CONFIGURED            IFF_RXFH_CONFIGURED
 
 /**
  *     struct net_device - The DEVICE structure.
@@ -4048,6 +4051,11 @@ static inline bool netif_is_lag_port(const struct net_device *dev)
        return netif_is_bond_slave(dev) || netif_is_team_port(dev);
 }
 
+static inline bool netif_is_rxfh_configured(const struct net_device *dev)
+{
+       return dev->priv_flags & IFF_RXFH_CONFIGURED;
+}
+
 /* This device needs to keep skb dst for qdisc enqueue or ndo_start_xmit() */
 static inline void netif_keep_dst(struct net_device *dev)
 {
index 453c803f1c8713da0138041e2b59534aae6b8206..379bdc59b1c8ac042b05732347d001df00854b18 100644 (file)
@@ -642,6 +642,37 @@ void netdev_rss_key_fill(void *buffer, size_t len)
 }
 EXPORT_SYMBOL(netdev_rss_key_fill);
 
+static int ethtool_get_max_rxfh_channel(struct net_device *dev, u32 *max)
+{
+       u32 dev_size, current_max = 0;
+       u32 *indir;
+       int ret;
+
+       if (!dev->ethtool_ops->get_rxfh_indir_size ||
+           !dev->ethtool_ops->get_rxfh)
+               return -EOPNOTSUPP;
+       dev_size = dev->ethtool_ops->get_rxfh_indir_size(dev);
+       if (dev_size == 0)
+               return -EOPNOTSUPP;
+
+       indir = kcalloc(dev_size, sizeof(indir[0]), GFP_USER);
+       if (!indir)
+               return -ENOMEM;
+
+       ret = dev->ethtool_ops->get_rxfh(dev, indir, NULL, NULL);
+       if (ret)
+               goto out;
+
+       while (dev_size--)
+               current_max = max(current_max, indir[dev_size]);
+
+       *max = current_max;
+
+out:
+       kfree(indir);
+       return ret;
+}
+
 static noinline_for_stack int ethtool_get_rxfh_indir(struct net_device *dev,
                                                     void __user *useraddr)
 {
@@ -738,6 +769,14 @@ static noinline_for_stack int ethtool_set_rxfh_indir(struct net_device *dev,
        }
 
        ret = ops->set_rxfh(dev, indir, NULL, ETH_RSS_HASH_NO_CHANGE);
+       if (ret)
+               goto out;
+
+       /* indicate whether rxfh was set to default */
+       if (user_size == 0)
+               dev->priv_flags &= ~IFF_RXFH_CONFIGURED;
+       else
+               dev->priv_flags |= IFF_RXFH_CONFIGURED;
 
 out:
        kfree(indir);
@@ -897,6 +936,14 @@ static noinline_for_stack int ethtool_set_rxfh(struct net_device *dev,
        }
 
        ret = ops->set_rxfh(dev, indir, hkey, rxfh.hfunc);
+       if (ret)
+               goto out;
+
+       /* indicate whether rxfh was set to default */
+       if (rxfh.indir_size == 0)
+               dev->priv_flags &= ~IFF_RXFH_CONFIGURED;
+       else if (rxfh.indir_size != ETH_RXFH_INDIR_NO_CHANGE)
+               dev->priv_flags |= IFF_RXFH_CONFIGURED;
 
 out:
        kfree(rss_config);
@@ -1228,6 +1275,7 @@ static noinline_for_stack int ethtool_set_channels(struct net_device *dev,
                                                   void __user *useraddr)
 {
        struct ethtool_channels channels;
+       u32 max_rx_in_use = 0;
 
        if (!dev->ethtool_ops->set_channels)
                return -EOPNOTSUPP;
@@ -1235,6 +1283,13 @@ static noinline_for_stack int ethtool_set_channels(struct net_device *dev,
        if (copy_from_user(&channels, useraddr, sizeof(channels)))
                return -EFAULT;
 
+       /* ensure the new Rx count fits within the configured Rx flow
+        * indirection table settings */
+       if (netif_is_rxfh_configured(dev) &&
+           !ethtool_get_max_rxfh_channel(dev, &max_rx_in_use) &&
+           (channels.combined_count + channels.rx_count) <= max_rx_in_use)
+           return -EINVAL;
+
        return dev->ethtool_ops->set_channels(dev, &channels);
 }