| From 79b4283e36464d7f138bacb05f4eff4716017866 Mon Sep 17 00:00:00 2001 |
| From: Sasha Levin <sashal@kernel.org> |
| Date: Mon, 8 Dec 2025 14:19:01 +0200 |
| Subject: ethtool: Avoid overflowing userspace buffer on stats query |
| |
| From: Gal Pressman <gal@nvidia.com> |
| |
| [ Upstream commit 7b07be1ff1cb6c49869910518650e8d0abc7d25f ] |
| |
| The ethtool -S command operates across three ioctl calls: |
| ETHTOOL_GSSET_INFO for the size, ETHTOOL_GSTRINGS for the names, and |
| ETHTOOL_GSTATS for the values. |
| |
| If the number of stats changes between these calls (e.g., due to device |
| reconfiguration), userspace's buffer allocation will be incorrect, |
| potentially leading to buffer overflow. |
| |
| Drivers are generally expected to maintain stable stat counts, but some |
| drivers (e.g., mlx5, bnx2x, bna, ksz884x) use dynamic counters, making |
| this scenario possible. |
| |
| Some drivers try to handle this internally: |
| - bnad_get_ethtool_stats() returns early in case stats.n_stats is not |
| equal to the driver's stats count. |
| - micrel/ksz884x also makes sure not to write anything beyond |
| stats.n_stats and overflow the buffer. |
| |
| However, both use stats.n_stats which is already assigned with the value |
| returned from get_sset_count(), hence won't solve the issue described |
| here. |
| |
| Change ethtool_get_strings(), ethtool_get_stats(), |
| ethtool_get_phy_stats() to not return anything in case of a mismatch |
| between userspace's size and get_sset_size(), to prevent buffer |
| overflow. |
| The returned n_stats value will be equal to zero, to reflect that |
| nothing has been returned. |
| |
| This could result in one of two cases when using upstream ethtool, |
| depending on when the size change is detected: |
| 1. When detected in ethtool_get_strings(): |
| # ethtool -S eth2 |
| no stats available |
| |
| 2. When detected in get stats, all stats will be reported as zero. |
| |
| Both cases are presumably transient, and a subsequent ethtool call |
| should succeed. |
| |
| Other than the overflow avoidance, these two cases are very evident (no |
| output/cleared stats), which is arguably better than presenting |
| incorrect/shifted stats. |
| I also considered returning an error instead of a "silent" response, but |
| that seems more destructive towards userspace apps. |
| |
| Notes: |
| - This patch does not claim to fix the inherent race, it only makes sure |
| that we do not overflow the userspace buffer, and makes for a more |
| predictable behavior. |
| |
| - RTNL lock is held during each ioctl, the race window exists between |
| the separate ioctl calls when the lock is released. |
| |
| - Userspace ethtool always fills stats.n_stats, but it is likely that |
| these stats ioctls are implemented in other userspace applications |
| which might not fill it. The added code checks that it's not zero, |
| to prevent any regressions. |
| |
| Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2") |
| Reviewed-by: Dragos Tatulea <dtatulea@nvidia.com> |
| Reviewed-by: Tariq Toukan <tariqt@nvidia.com> |
| Signed-off-by: Gal Pressman <gal@nvidia.com> |
| Link: https://patch.msgid.link/20251208121901.3203692-1-gal@nvidia.com |
| Signed-off-by: Paolo Abeni <pabeni@redhat.com> |
| Signed-off-by: Sasha Levin <sashal@kernel.org> |
| --- |
| net/ethtool/ioctl.c | 30 ++++++++++++++++++++++++------ |
| 1 file changed, 24 insertions(+), 6 deletions(-) |
| |
| diff --git a/net/ethtool/ioctl.c b/net/ethtool/ioctl.c |
| index 4486cbe2faf0c..eaeb514b7e5f6 100644 |
| --- a/net/ethtool/ioctl.c |
| +++ b/net/ethtool/ioctl.c |
| @@ -1955,7 +1955,10 @@ static int ethtool_get_strings(struct net_device *dev, void __user *useraddr) |
| return -ENOMEM; |
| WARN_ON_ONCE(!ret); |
| |
| - gstrings.len = ret; |
| + if (gstrings.len && gstrings.len != ret) |
| + gstrings.len = 0; |
| + else |
| + gstrings.len = ret; |
| |
| if (gstrings.len) { |
| data = vzalloc(array_size(gstrings.len, ETH_GSTRING_LEN)); |
| @@ -2070,10 +2073,13 @@ static int ethtool_get_stats(struct net_device *dev, void __user *useraddr) |
| if (copy_from_user(&stats, useraddr, sizeof(stats))) |
| return -EFAULT; |
| |
| - stats.n_stats = n_stats; |
| + if (stats.n_stats && stats.n_stats != n_stats) |
| + stats.n_stats = 0; |
| + else |
| + stats.n_stats = n_stats; |
| |
| - if (n_stats) { |
| - data = vzalloc(array_size(n_stats, sizeof(u64))); |
| + if (stats.n_stats) { |
| + data = vzalloc(array_size(stats.n_stats, sizeof(u64))); |
| if (!data) |
| return -ENOMEM; |
| ops->get_ethtool_stats(dev, &stats, data); |
| @@ -2085,7 +2091,9 @@ static int ethtool_get_stats(struct net_device *dev, void __user *useraddr) |
| if (copy_to_user(useraddr, &stats, sizeof(stats))) |
| goto out; |
| useraddr += sizeof(stats); |
| - if (n_stats && copy_to_user(useraddr, data, array_size(n_stats, sizeof(u64)))) |
| + if (stats.n_stats && |
| + copy_to_user(useraddr, data, |
| + array_size(stats.n_stats, sizeof(u64)))) |
| goto out; |
| ret = 0; |
| |
| @@ -2121,6 +2129,10 @@ static int ethtool_get_phy_stats_phydev(struct phy_device *phydev, |
| return -EOPNOTSUPP; |
| |
| n_stats = phy_ops->get_sset_count(phydev); |
| + if (stats->n_stats && stats->n_stats != n_stats) { |
| + stats->n_stats = 0; |
| + return 0; |
| + } |
| |
| ret = ethtool_vzalloc_stats_array(n_stats, data); |
| if (ret) |
| @@ -2141,6 +2153,10 @@ static int ethtool_get_phy_stats_ethtool(struct net_device *dev, |
| return -EOPNOTSUPP; |
| |
| n_stats = ops->get_sset_count(dev, ETH_SS_PHY_STATS); |
| + if (stats->n_stats && stats->n_stats != n_stats) { |
| + stats->n_stats = 0; |
| + return 0; |
| + } |
| |
| ret = ethtool_vzalloc_stats_array(n_stats, data); |
| if (ret) |
| @@ -2177,7 +2193,9 @@ static int ethtool_get_phy_stats(struct net_device *dev, void __user *useraddr) |
| } |
| |
| useraddr += sizeof(stats); |
| - if (copy_to_user(useraddr, data, array_size(stats.n_stats, sizeof(u64)))) |
| + if (stats.n_stats && |
| + copy_to_user(useraddr, data, |
| + array_size(stats.n_stats, sizeof(u64)))) |
| ret = -EFAULT; |
| |
| out: |
| -- |
| 2.51.0 |
| |