Files
linux/drivers/net/ifb.c
Michael Bommarito 5db89c9956 net: ifb: report ethtool stats over num_tx_queues
ifb_dev_init() allocates dp->tx_private to dev->num_tx_queues
entries via kzalloc_objs(*txp, dev->num_tx_queues). Both IFB
per-queue RX and TX stats live in those entries: ifb_xmit() updates
txp->rx_stats using the skb queue mapping, ifb_ri_tasklet() updates
txp->tx_stats, and ifb_stats64() aggregates both over
dev->num_tx_queues.

The ethtool stats callbacks instead size and walk the per-queue
stats with dev->real_num_rx_queues and dev->real_num_tx_queues. With
an asymmetric device where the RX queue count exceeds the TX queue
count, for example:

    ip link add name ifb10 numtxqueues 1 numrxqueues 8 type ifb
    ethtool -S ifb10

ifb_get_ethtool_stats() indexes past the tx_private allocation and
copies adjacent slab data through ETHTOOL_GSTATS.

Use dev->num_tx_queues consistently for the stats strings, the
stats count, and the stats data walks. This reports one RX stats
group and one TX stats group for each backing ifb_q_private entry,
which is the queue set IFB can actually populate.

Reproduced under UML+KASAN at v7.1-rc2:

  BUG: KASAN: slab-out-of-bounds in ifb_fill_stats_data+0x3c/0xae
  Read of size 8 at addr 0000000062dbd228 by task ethtool/36
  ifb_fill_stats_data+0x3c/0xae
  ifb_get_ethtool_stats+0xc0/0x129
  __dev_ethtool+0x1ca5/0x363c
  dev_ethtool+0x123/0x1b3
  dev_ioctl+0x56c/0x744
  sock_do_ioctl+0x15f/0x1b2
  sock_ioctl+0x4d5/0x50a
  sys_ioctl+0xd8b/0xde9

With the patch applied, the same UML+KASAN repro is silent and
ethtool -S ifb10 reports only the stats backed by the single
allocated tx_private entry.

Fixes: a21ee5b2fc ("net: ifb: support ethtools stats")
Cc: stable@vger.kernel.org
Signed-off-by: Michael Bommarito <michael.bommarito@gmail.com>
Link: https://patch.msgid.link/20260514013739.3549624-1-michael.bommarito@gmail.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
2026-05-14 18:40:04 -07:00

459 lines
11 KiB
C

// SPDX-License-Identifier: GPL-2.0-or-later
/* drivers/net/ifb.c:
The purpose of this driver is to provide a device that allows
for sharing of resources:
1) qdiscs/policies that are per device as opposed to system wide.
ifb allows for a device which can be redirected to thus providing
an impression of sharing.
2) Allows for queueing incoming traffic for shaping instead of
dropping.
The original concept is based on what is known as the IMQ
driver initially written by Martin Devera, later rewritten
by Patrick McHardy and then maintained by Andre Correa.
You need the tc action mirror or redirect to feed this device
packets.
Authors: Jamal Hadi Salim (2005)
*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/netdevice.h>
#include <linux/ethtool.h>
#include <linux/etherdevice.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/moduleparam.h>
#include <linux/netfilter_netdev.h>
#include <net/pkt_sched.h>
#include <net/net_namespace.h>
#define TX_Q_LIMIT 32
struct ifb_q_stats {
u64_stats_t packets;
u64_stats_t bytes;
struct u64_stats_sync sync;
};
struct ifb_q_private {
struct net_device *dev;
struct tasklet_struct ifb_tasklet;
int tasklet_pending;
int txqnum;
struct sk_buff_head rq;
struct sk_buff_head tq;
struct ifb_q_stats rx_stats;
struct ifb_q_stats tx_stats;
} ____cacheline_aligned_in_smp;
struct ifb_dev_private {
struct ifb_q_private *tx_private;
};
/* For ethtools stats. */
struct ifb_q_stats_desc {
char desc[ETH_GSTRING_LEN];
size_t offset;
};
#define IFB_Q_STAT(m) offsetof(struct ifb_q_stats, m)
static const struct ifb_q_stats_desc ifb_q_stats_desc[] = {
{ "packets", IFB_Q_STAT(packets) },
{ "bytes", IFB_Q_STAT(bytes) },
};
#define IFB_Q_STATS_LEN ARRAY_SIZE(ifb_q_stats_desc)
static netdev_tx_t ifb_xmit(struct sk_buff *skb, struct net_device *dev);
static int ifb_open(struct net_device *dev);
static int ifb_close(struct net_device *dev);
static void ifb_update_q_stats(struct ifb_q_stats *stats, int len)
{
u64_stats_update_begin(&stats->sync);
u64_stats_inc(&stats->packets);
u64_stats_add(&stats->bytes, len);
u64_stats_update_end(&stats->sync);
}
static void ifb_ri_tasklet(struct tasklet_struct *t)
{
struct ifb_q_private *txp = from_tasklet(txp, t, ifb_tasklet);
struct netdev_queue *txq;
struct sk_buff *skb;
txq = netdev_get_tx_queue(txp->dev, txp->txqnum);
skb = skb_peek(&txp->tq);
if (!skb) {
if (!__netif_tx_trylock(txq))
goto resched;
skb_queue_splice_tail_init(&txp->rq, &txp->tq);
__netif_tx_unlock(txq);
}
while ((skb = __skb_dequeue(&txp->tq)) != NULL) {
/* Skip tc and netfilter to prevent redirection loop. */
skb->redirected = 0;
#ifdef CONFIG_NET_CLS_ACT
skb->tc_skip_classify = 1;
#endif
nf_skip_egress(skb, true);
ifb_update_q_stats(&txp->tx_stats, skb->len);
rcu_read_lock();
skb->dev = dev_get_by_index_rcu(dev_net(txp->dev), skb->skb_iif);
if (!skb->dev) {
rcu_read_unlock();
dev_kfree_skb(skb);
txp->dev->stats.tx_dropped++;
if (skb_queue_len(&txp->tq) != 0)
goto resched;
break;
}
rcu_read_unlock();
skb->skb_iif = txp->dev->ifindex;
if (!skb->from_ingress) {
dev_queue_xmit(skb);
} else {
skb_pull_rcsum(skb, skb->mac_len);
netif_receive_skb(skb);
}
}
if (__netif_tx_trylock(txq)) {
skb = skb_peek(&txp->rq);
if (!skb) {
txp->tasklet_pending = 0;
if (netif_tx_queue_stopped(txq))
netif_tx_wake_queue(txq);
} else {
__netif_tx_unlock(txq);
goto resched;
}
__netif_tx_unlock(txq);
} else {
resched:
txp->tasklet_pending = 1;
tasklet_schedule(&txp->ifb_tasklet);
}
}
static void ifb_stats64(struct net_device *dev,
struct rtnl_link_stats64 *stats)
{
struct ifb_dev_private *dp = netdev_priv(dev);
struct ifb_q_private *txp = dp->tx_private;
unsigned int start;
u64 packets, bytes;
int i;
for (i = 0; i < dev->num_tx_queues; i++,txp++) {
do {
start = u64_stats_fetch_begin(&txp->rx_stats.sync);
packets = u64_stats_read(&txp->rx_stats.packets);
bytes = u64_stats_read(&txp->rx_stats.bytes);
} while (u64_stats_fetch_retry(&txp->rx_stats.sync, start));
stats->rx_packets += packets;
stats->rx_bytes += bytes;
do {
start = u64_stats_fetch_begin(&txp->tx_stats.sync);
packets = u64_stats_read(&txp->tx_stats.packets);
bytes = u64_stats_read(&txp->tx_stats.bytes);
} while (u64_stats_fetch_retry(&txp->tx_stats.sync, start));
stats->tx_packets += packets;
stats->tx_bytes += bytes;
}
stats->rx_dropped = dev->stats.rx_dropped;
stats->tx_dropped = dev->stats.tx_dropped;
}
static int ifb_dev_init(struct net_device *dev)
{
struct ifb_dev_private *dp = netdev_priv(dev);
struct ifb_q_private *txp;
int i;
txp = kzalloc_objs(*txp, dev->num_tx_queues);
if (!txp)
return -ENOMEM;
dp->tx_private = txp;
for (i = 0; i < dev->num_tx_queues; i++,txp++) {
txp->txqnum = i;
txp->dev = dev;
__skb_queue_head_init(&txp->rq);
__skb_queue_head_init(&txp->tq);
u64_stats_init(&txp->rx_stats.sync);
u64_stats_init(&txp->tx_stats.sync);
tasklet_setup(&txp->ifb_tasklet, ifb_ri_tasklet);
netif_tx_start_queue(netdev_get_tx_queue(dev, i));
}
return 0;
}
static void ifb_get_strings(struct net_device *dev, u32 stringset, u8 *buf)
{
u8 *p = buf;
int i, j;
switch (stringset) {
case ETH_SS_STATS:
for (i = 0; i < dev->num_tx_queues; i++)
for (j = 0; j < IFB_Q_STATS_LEN; j++)
ethtool_sprintf(&p, "rx_queue_%u_%.18s",
i, ifb_q_stats_desc[j].desc);
for (i = 0; i < dev->num_tx_queues; i++)
for (j = 0; j < IFB_Q_STATS_LEN; j++)
ethtool_sprintf(&p, "tx_queue_%u_%.18s",
i, ifb_q_stats_desc[j].desc);
break;
}
}
static int ifb_get_sset_count(struct net_device *dev, int sset)
{
switch (sset) {
case ETH_SS_STATS:
return IFB_Q_STATS_LEN * dev->num_tx_queues * 2;
default:
return -EOPNOTSUPP;
}
}
static void ifb_fill_stats_data(u64 **data,
struct ifb_q_stats *q_stats)
{
void *stats_base = (void *)q_stats;
unsigned int start;
size_t offset;
int j;
do {
start = u64_stats_fetch_begin(&q_stats->sync);
for (j = 0; j < IFB_Q_STATS_LEN; j++) {
offset = ifb_q_stats_desc[j].offset;
(*data)[j] = u64_stats_read((u64_stats_t *)(stats_base + offset));
}
} while (u64_stats_fetch_retry(&q_stats->sync, start));
*data += IFB_Q_STATS_LEN;
}
static void ifb_get_ethtool_stats(struct net_device *dev,
struct ethtool_stats *stats, u64 *data)
{
struct ifb_dev_private *dp = netdev_priv(dev);
struct ifb_q_private *txp;
int i;
for (i = 0; i < dev->num_tx_queues; i++) {
txp = dp->tx_private + i;
ifb_fill_stats_data(&data, &txp->rx_stats);
}
for (i = 0; i < dev->num_tx_queues; i++) {
txp = dp->tx_private + i;
ifb_fill_stats_data(&data, &txp->tx_stats);
}
}
static const struct net_device_ops ifb_netdev_ops = {
.ndo_open = ifb_open,
.ndo_stop = ifb_close,
.ndo_get_stats64 = ifb_stats64,
.ndo_start_xmit = ifb_xmit,
.ndo_validate_addr = eth_validate_addr,
.ndo_init = ifb_dev_init,
};
static const struct ethtool_ops ifb_ethtool_ops = {
.get_strings = ifb_get_strings,
.get_sset_count = ifb_get_sset_count,
.get_ethtool_stats = ifb_get_ethtool_stats,
};
#define IFB_FEATURES (NETIF_F_HW_CSUM | NETIF_F_SG | NETIF_F_FRAGLIST | \
NETIF_F_GSO_SOFTWARE | NETIF_F_GSO_ENCAP_ALL | \
NETIF_F_HIGHDMA | NETIF_F_HW_VLAN_CTAG_TX | \
NETIF_F_HW_VLAN_STAG_TX)
static void ifb_dev_free(struct net_device *dev)
{
struct ifb_dev_private *dp = netdev_priv(dev);
struct ifb_q_private *txp = dp->tx_private;
int i;
for (i = 0; i < dev->num_tx_queues; i++,txp++) {
tasklet_kill(&txp->ifb_tasklet);
__skb_queue_purge(&txp->rq);
__skb_queue_purge(&txp->tq);
}
kfree(dp->tx_private);
}
static void ifb_setup(struct net_device *dev)
{
/* Initialize the device structure. */
dev->netdev_ops = &ifb_netdev_ops;
dev->ethtool_ops = &ifb_ethtool_ops;
/* Fill in device structure with ethernet-generic values. */
ether_setup(dev);
dev->tx_queue_len = TX_Q_LIMIT;
dev->features |= IFB_FEATURES;
dev->hw_features |= dev->features;
dev->hw_enc_features |= dev->features;
dev->vlan_features |= IFB_FEATURES & ~(NETIF_F_HW_VLAN_CTAG_TX |
NETIF_F_HW_VLAN_STAG_TX);
dev->flags |= IFF_NOARP;
dev->flags &= ~IFF_MULTICAST;
dev->priv_flags &= ~IFF_TX_SKB_SHARING;
netif_keep_dst(dev);
eth_hw_addr_random(dev);
dev->needs_free_netdev = true;
dev->priv_destructor = ifb_dev_free;
dev->min_mtu = 0;
dev->max_mtu = 0;
netif_set_tso_max_size(dev, GSO_MAX_SIZE);
}
static netdev_tx_t ifb_xmit(struct sk_buff *skb, struct net_device *dev)
{
struct ifb_dev_private *dp = netdev_priv(dev);
struct ifb_q_private *txp = dp->tx_private + skb_get_queue_mapping(skb);
ifb_update_q_stats(&txp->rx_stats, skb->len);
if (!skb->redirected || !skb->skb_iif) {
dev_kfree_skb(skb);
dev->stats.rx_dropped++;
return NETDEV_TX_OK;
}
if (skb_queue_len(&txp->rq) >= dev->tx_queue_len)
netif_tx_stop_queue(netdev_get_tx_queue(dev, txp->txqnum));
__skb_queue_tail(&txp->rq, skb);
if (!txp->tasklet_pending) {
txp->tasklet_pending = 1;
tasklet_schedule(&txp->ifb_tasklet);
}
return NETDEV_TX_OK;
}
static int ifb_close(struct net_device *dev)
{
netif_tx_stop_all_queues(dev);
return 0;
}
static int ifb_open(struct net_device *dev)
{
netif_tx_start_all_queues(dev);
return 0;
}
static int ifb_validate(struct nlattr *tb[], struct nlattr *data[],
struct netlink_ext_ack *extack)
{
if (tb[IFLA_ADDRESS]) {
if (nla_len(tb[IFLA_ADDRESS]) != ETH_ALEN)
return -EINVAL;
if (!is_valid_ether_addr(nla_data(tb[IFLA_ADDRESS])))
return -EADDRNOTAVAIL;
}
return 0;
}
static struct rtnl_link_ops ifb_link_ops __read_mostly = {
.kind = "ifb",
.priv_size = sizeof(struct ifb_dev_private),
.setup = ifb_setup,
.validate = ifb_validate,
};
/* Number of ifb devices to be set up by this module.
* Note that these legacy devices have one queue.
* Prefer something like : ip link add ifb10 numtxqueues 8 type ifb
*/
static int numifbs = 2;
module_param(numifbs, int, 0);
MODULE_PARM_DESC(numifbs, "Number of ifb devices");
static int __init ifb_init_one(int index)
{
struct net_device *dev_ifb;
int err;
dev_ifb = alloc_netdev(sizeof(struct ifb_dev_private), "ifb%d",
NET_NAME_UNKNOWN, ifb_setup);
if (!dev_ifb)
return -ENOMEM;
dev_ifb->rtnl_link_ops = &ifb_link_ops;
err = register_netdevice(dev_ifb);
if (err < 0)
goto err;
return 0;
err:
free_netdev(dev_ifb);
return err;
}
static int __init ifb_init_module(void)
{
int i, err;
err = rtnl_link_register(&ifb_link_ops);
if (err < 0)
return err;
rtnl_net_lock(&init_net);
for (i = 0; i < numifbs && !err; i++) {
err = ifb_init_one(i);
cond_resched();
}
rtnl_net_unlock(&init_net);
if (err)
rtnl_link_unregister(&ifb_link_ops);
return err;
}
static void __exit ifb_cleanup_module(void)
{
rtnl_link_unregister(&ifb_link_ops);
}
module_init(ifb_init_module);
module_exit(ifb_cleanup_module);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Intermediate Functional Block (ifb) netdevice driver for sharing of resources and ingress packet queuing");
MODULE_AUTHOR("Jamal Hadi Salim");
MODULE_ALIAS_RTNL_LINK("ifb");