ethtool: Add generic options for tunables
[cascardo/linux.git] / net / core / ethtool.c
index 640ba0e..27e61b8 100644 (file)
@@ -557,6 +557,23 @@ err_out:
        return ret;
 }
 
+static int ethtool_copy_validate_indir(u32 *indir, void __user *useraddr,
+                                       struct ethtool_rxnfc *rx_rings,
+                                       u32 size)
+{
+       int i;
+
+       if (copy_from_user(indir, useraddr, size * sizeof(indir[0])))
+               return -EFAULT;
+
+       /* Validate ring indices */
+       for (i = 0; i < size; i++)
+               if (indir[i] >= rx_rings->data)
+                       return -EINVAL;
+
+       return 0;
+}
+
 static noinline_for_stack int ethtool_get_rxfh_indir(struct net_device *dev,
                                                     void __user *useraddr)
 {
@@ -565,7 +582,7 @@ static noinline_for_stack int ethtool_get_rxfh_indir(struct net_device *dev,
        int ret;
 
        if (!dev->ethtool_ops->get_rxfh_indir_size ||
-           !dev->ethtool_ops->get_rxfh_indir)
+           !dev->ethtool_ops->get_rxfh)
                return -EOPNOTSUPP;
        dev_size = dev->ethtool_ops->get_rxfh_indir_size(dev);
        if (dev_size == 0)
@@ -591,7 +608,7 @@ static noinline_for_stack int ethtool_get_rxfh_indir(struct net_device *dev,
        if (!indir)
                return -ENOMEM;
 
-       ret = dev->ethtool_ops->get_rxfh_indir(dev, indir);
+       ret = dev->ethtool_ops->get_rxfh(dev, indir, NULL);
        if (ret)
                goto out;
 
@@ -613,8 +630,9 @@ static noinline_for_stack int ethtool_set_rxfh_indir(struct net_device *dev,
        u32 *indir;
        const struct ethtool_ops *ops = dev->ethtool_ops;
        int ret;
+       u32 ringidx_offset = offsetof(struct ethtool_rxfh_indir, ring_index[0]);
 
-       if (!ops->get_rxfh_indir_size || !ops->set_rxfh_indir ||
+       if (!ops->get_rxfh_indir_size || !ops->set_rxfh ||
            !ops->get_rxnfc)
                return -EOPNOTSUPP;
 
@@ -643,28 +661,184 @@ static noinline_for_stack int ethtool_set_rxfh_indir(struct net_device *dev,
                for (i = 0; i < dev_size; i++)
                        indir[i] = ethtool_rxfh_indir_default(i, rx_rings.data);
        } else {
-               if (copy_from_user(indir,
-                                 useraddr +
-                                 offsetof(struct ethtool_rxfh_indir,
-                                          ring_index[0]),
-                                 dev_size * sizeof(indir[0]))) {
+               ret = ethtool_copy_validate_indir(indir,
+                                                 useraddr + ringidx_offset,
+                                                 &rx_rings,
+                                                 dev_size);
+               if (ret)
+                       goto out;
+       }
+
+       ret = ops->set_rxfh(dev, indir, NULL);
+
+out:
+       kfree(indir);
+       return ret;
+}
+
+static noinline_for_stack int ethtool_get_rxfh(struct net_device *dev,
+                                              void __user *useraddr)
+{
+       int ret;
+       const struct ethtool_ops *ops = dev->ethtool_ops;
+       u32 user_indir_size, user_key_size;
+       u32 dev_indir_size = 0, dev_key_size = 0;
+       struct ethtool_rxfh rxfh;
+       u32 total_size;
+       u32 indir_bytes;
+       u32 *indir = NULL;
+       u8 *hkey = NULL;
+       u8 *rss_config;
+
+       if (!(dev->ethtool_ops->get_rxfh_indir_size ||
+             dev->ethtool_ops->get_rxfh_key_size) ||
+             !dev->ethtool_ops->get_rxfh)
+               return -EOPNOTSUPP;
+
+       if (ops->get_rxfh_indir_size)
+               dev_indir_size = ops->get_rxfh_indir_size(dev);
+       if (ops->get_rxfh_key_size)
+               dev_key_size = ops->get_rxfh_key_size(dev);
+
+       if ((dev_key_size + dev_indir_size) == 0)
+               return -EOPNOTSUPP;
+
+       if (copy_from_user(&rxfh, useraddr, sizeof(rxfh)))
+               return -EFAULT;
+       user_indir_size = rxfh.indir_size;
+       user_key_size = rxfh.key_size;
+
+       /* Check that reserved fields are 0 for now */
+       if (rxfh.rss_context || rxfh.rsvd[0] || rxfh.rsvd[1])
+               return -EINVAL;
+
+       rxfh.indir_size = dev_indir_size;
+       rxfh.key_size = dev_key_size;
+       if (copy_to_user(useraddr, &rxfh, sizeof(rxfh)))
+               return -EFAULT;
+
+       /* If the user buffer size is 0, this is just a query for the
+        * device table size and key size.  Otherwise, if the User size is
+        * not equal to device table size or key size it's an error.
+        */
+       if (!user_indir_size && !user_key_size)
+               return 0;
+
+       if ((user_indir_size && (user_indir_size != dev_indir_size)) ||
+           (user_key_size && (user_key_size != dev_key_size)))
+               return -EINVAL;
+
+       indir_bytes = user_indir_size * sizeof(indir[0]);
+       total_size = indir_bytes + user_key_size;
+       rss_config = kzalloc(total_size, GFP_USER);
+       if (!rss_config)
+               return -ENOMEM;
+
+       if (user_indir_size)
+               indir = (u32 *)rss_config;
+
+       if (user_key_size)
+               hkey = rss_config + indir_bytes;
+
+       ret = dev->ethtool_ops->get_rxfh(dev, indir, hkey);
+       if (!ret) {
+               if (copy_to_user(useraddr +
+                                offsetof(struct ethtool_rxfh, rss_config[0]),
+                                rss_config, total_size))
                        ret = -EFAULT;
+       }
+
+       kfree(rss_config);
+
+       return ret;
+}
+
+static noinline_for_stack int ethtool_set_rxfh(struct net_device *dev,
+                                              void __user *useraddr)
+{
+       int ret;
+       const struct ethtool_ops *ops = dev->ethtool_ops;
+       struct ethtool_rxnfc rx_rings;
+       struct ethtool_rxfh rxfh;
+       u32 dev_indir_size = 0, dev_key_size = 0, i;
+       u32 *indir = NULL, indir_bytes = 0;
+       u8 *hkey = NULL;
+       u8 *rss_config;
+       u32 rss_cfg_offset = offsetof(struct ethtool_rxfh, rss_config[0]);
+
+       if (!(ops->get_rxfh_indir_size || ops->get_rxfh_key_size) ||
+           !ops->get_rxnfc || !ops->set_rxfh)
+               return -EOPNOTSUPP;
+
+       if (ops->get_rxfh_indir_size)
+               dev_indir_size = ops->get_rxfh_indir_size(dev);
+       if (ops->get_rxfh_key_size)
+               dev_key_size = dev->ethtool_ops->get_rxfh_key_size(dev);
+       if ((dev_key_size + dev_indir_size) == 0)
+               return -EOPNOTSUPP;
+
+       if (copy_from_user(&rxfh, useraddr, sizeof(rxfh)))
+               return -EFAULT;
+
+       /* Check that reserved fields are 0 for now */
+       if (rxfh.rss_context || rxfh.rsvd[0] || rxfh.rsvd[1])
+               return -EINVAL;
+
+       /* If either indir or hash key is valid, proceed further.
+        * It is not valid to request that both be unchanged.
+        */
+       if ((rxfh.indir_size &&
+            rxfh.indir_size != ETH_RXFH_INDIR_NO_CHANGE &&
+            rxfh.indir_size != dev_indir_size) ||
+           (rxfh.key_size && (rxfh.key_size != dev_key_size)) ||
+           (rxfh.indir_size == ETH_RXFH_INDIR_NO_CHANGE &&
+            rxfh.key_size == 0))
+               return -EINVAL;
+
+       if (rxfh.indir_size != ETH_RXFH_INDIR_NO_CHANGE)
+               indir_bytes = dev_indir_size * sizeof(indir[0]);
+
+       rss_config = kzalloc(indir_bytes + rxfh.key_size, GFP_USER);
+       if (!rss_config)
+               return -ENOMEM;
+
+       rx_rings.cmd = ETHTOOL_GRXRINGS;
+       ret = ops->get_rxnfc(dev, &rx_rings, NULL);
+       if (ret)
+               goto out;
+
+       /* rxfh.indir_size == 0 means reset the indir table to default.
+        * rxfh.indir_size == ETH_RXFH_INDIR_NO_CHANGE means leave it unchanged.
+        */
+       if (rxfh.indir_size &&
+           rxfh.indir_size != ETH_RXFH_INDIR_NO_CHANGE) {
+               indir = (u32 *)rss_config;
+               ret = ethtool_copy_validate_indir(indir,
+                                                 useraddr + rss_cfg_offset,
+                                                 &rx_rings,
+                                                 rxfh.indir_size);
+               if (ret)
                        goto out;
-               }
+       } else if (rxfh.indir_size == 0) {
+               indir = (u32 *)rss_config;
+               for (i = 0; i < dev_indir_size; i++)
+                       indir[i] = ethtool_rxfh_indir_default(i, rx_rings.data);
+       }
 
-               /* Validate ring indices */
-               for (i = 0; i < dev_size; i++) {
-                       if (indir[i] >= rx_rings.data) {
-                               ret = -EINVAL;
-                               goto out;
-                       }
+       if (rxfh.key_size) {
+               hkey = rss_config + indir_bytes;
+               if (copy_from_user(hkey,
+                                  useraddr + rss_cfg_offset + indir_bytes,
+                                  rxfh.key_size)) {
+                       ret = -EFAULT;
+                       goto out;
                }
        }
 
-       ret = ops->set_rxfh_indir(dev, indir);
+       ret = ops->set_rxfh(dev, indir, hkey);
 
 out:
-       kfree(indir);
+       kfree(rss_config);
        return ret;
 }
 
@@ -1447,6 +1621,80 @@ static int ethtool_get_module_eeprom(struct net_device *dev,
                                      modinfo.eeprom_len);
 }
 
+static int ethtool_tunable_valid(const struct ethtool_tunable *tuna)
+{
+       switch (tuna->id) {
+       case ETHTOOL_RX_COPYBREAK:
+               if (tuna->len != sizeof(u32) ||
+                   tuna->type_id != ETHTOOL_TUNABLE_U32)
+                       return -EINVAL;
+               break;
+       default:
+               return -EINVAL;
+       }
+
+       return 0;
+}
+
+static int ethtool_get_tunable(struct net_device *dev, void __user *useraddr)
+{
+       int ret;
+       struct ethtool_tunable tuna;
+       const struct ethtool_ops *ops = dev->ethtool_ops;
+       void *data;
+
+       if (!ops->get_tunable)
+               return -EOPNOTSUPP;
+       if (copy_from_user(&tuna, useraddr, sizeof(tuna)))
+               return -EFAULT;
+       ret = ethtool_tunable_valid(&tuna);
+       if (ret)
+               return ret;
+       data = kmalloc(tuna.len, GFP_USER);
+       if (!data)
+               return -ENOMEM;
+       ret = ops->get_tunable(dev, &tuna, data);
+       if (ret)
+               goto out;
+       useraddr += sizeof(tuna);
+       ret = -EFAULT;
+       if (copy_to_user(useraddr, data, tuna.len))
+               goto out;
+       ret = 0;
+
+out:
+       kfree(data);
+       return ret;
+}
+
+static int ethtool_set_tunable(struct net_device *dev, void __user *useraddr)
+{
+       int ret;
+       struct ethtool_tunable tuna;
+       const struct ethtool_ops *ops = dev->ethtool_ops;
+       void *data;
+
+       if (!ops->set_tunable)
+               return -EOPNOTSUPP;
+       if (copy_from_user(&tuna, useraddr, sizeof(tuna)))
+               return -EFAULT;
+       ret = ethtool_tunable_valid(&tuna);
+       if (ret)
+               return ret;
+       data = kmalloc(tuna.len, GFP_USER);
+       if (!data)
+               return -ENOMEM;
+       useraddr += sizeof(tuna);
+       ret = -EFAULT;
+       if (copy_from_user(data, useraddr, tuna.len))
+               goto out;
+       ret = ops->set_tunable(dev, &tuna, data);
+
+out:
+       kfree(data);
+       return ret;
+}
+
 /* The main entry point in this file.  Called from net/core/dev_ioctl.c */
 
 int dev_ethtool(struct net *net, struct ifreq *ifr)
@@ -1491,10 +1739,12 @@ int dev_ethtool(struct net *net, struct ifreq *ifr)
        case ETHTOOL_GRXCLSRULE:
        case ETHTOOL_GRXCLSRLALL:
        case ETHTOOL_GRXFHINDIR:
+       case ETHTOOL_GRSSH:
        case ETHTOOL_GFEATURES:
        case ETHTOOL_GCHANNELS:
        case ETHTOOL_GET_TS_INFO:
        case ETHTOOL_GEEE:
+       case ETHTOOL_GTUNABLE:
                break;
        default:
                if (!ns_capable(net->user_ns, CAP_NET_ADMIN))
@@ -1628,6 +1878,12 @@ int dev_ethtool(struct net *net, struct ifreq *ifr)
        case ETHTOOL_SRXFHINDIR:
                rc = ethtool_set_rxfh_indir(dev, useraddr);
                break;
+       case ETHTOOL_GRSSH:
+               rc = ethtool_get_rxfh(dev, useraddr);
+               break;
+       case ETHTOOL_SRSSH:
+               rc = ethtool_set_rxfh(dev, useraddr);
+               break;
        case ETHTOOL_GFEATURES:
                rc = ethtool_get_features(dev, useraddr);
                break;
@@ -1676,6 +1932,12 @@ int dev_ethtool(struct net *net, struct ifreq *ifr)
        case ETHTOOL_GMODULEEEPROM:
                rc = ethtool_get_module_eeprom(dev, useraddr);
                break;
+       case ETHTOOL_GTUNABLE:
+               rc = ethtool_get_tunable(dev, useraddr);
+               break;
+       case ETHTOOL_STUNABLE:
+               rc = ethtool_set_tunable(dev, useraddr);
+               break;
        default:
                rc = -EOPNOTSUPP;
        }