net/sched: sch_teql: Introduce slaves_lock to avoid race condition and UAF

The teql master->slaves singly linked list is not protected against
multiple writes. It can be mod'ed concurently from teql_master_xmit(),
teql_dequeue(), teql_init() and teql_destroy() without holding any list
lock or RCU protection.

zdi-disclosures@trendmicro.com has demonstrated that the qdisc is freed
after an RCU grace period, but teql_master_xmit() running on another
CPU can still hold a stale pointer into the list, resulting in a
slab-use-after-free:

BUG: KASAN: slab-use-after-free in teql_master_xmit+0xf0f/0x16b0
Read of size 8 at addr ffff888013fb0440 by task poc/332
Freed 512-byte region [ffff888013fb0400, ffff888013fb0600) (kmalloc-512)

The fix?
Add a per-master slaves_lock spinlock that serializes all mutations of
master->slaves and the NEXT_SLAVE() links in teql_destroy() and
teql_qdisc_init(). teql_master_xmit() also takes the same slaves_lock
around those updates.
Annotate master->slaves and the per-slave ->next pointer with __rcu and
use the appropriate RCU accessors everywhere they are touched:
rcu_assign_pointer() on the writer side (under slaves_lock),
rcu_dereference_protected() for the writer-side loads (also under
slaves_lock), rcu_dereference_bh() for the loads in teql_master_xmit() and
rtnl_dereference() for the loads in teql_master_open()/teql_master_mtu(),
which run under RTNL.
Pair this with rcu_read_lock_bh()/rcu_read_unlock_bh() around the list
traversal in teql_master_xmit(), so that readers either observe a fully
linked list or are deferred until the in-flight mutation completes. The two
early-return paths in teql_master_xmit() are updated to release the RCU-bh
read-side critical section before returning, since leaving it held would
disable BH on that CPU for good.

Fixes: 1da177e4c3 ("Linux-2.6.12-rc2")
Reported-by: zdi-disclosures@trendmicro.com
Tested-by: Victor Nogueira <victor@mojatatu.com>
Signed-off-by: Jamal Hadi Salim <jhs@mojatatu.com>
Link: https://patch.msgid.link/20260628111229.669751-1-jhs@mojatatu.com
Signed-off-by: Paolo Abeni <pabeni@redhat.com>
This commit is contained in:
Jamal Hadi Salim 2026-06-28 07:12:29 -04:00 committed by Paolo Abeni
parent dbf803bc4a
commit e5b811fe79

View File

@ -52,7 +52,8 @@
struct teql_master { struct teql_master {
struct Qdisc_ops qops; struct Qdisc_ops qops;
struct net_device *dev; struct net_device *dev;
struct Qdisc *slaves; struct Qdisc __rcu *slaves;
spinlock_t slaves_lock; /* serializes writes to ->slaves */
struct list_head master_list; struct list_head master_list;
unsigned long tx_bytes; unsigned long tx_bytes;
unsigned long tx_packets; unsigned long tx_packets;
@ -61,7 +62,7 @@ struct teql_master {
}; };
struct teql_sched_data { struct teql_sched_data {
struct Qdisc *next; struct Qdisc __rcu *next;
struct teql_master *m; struct teql_master *m;
struct sk_buff_head q; struct sk_buff_head q;
}; };
@ -101,7 +102,9 @@ teql_dequeue(struct Qdisc *sch)
if (skb == NULL) { if (skb == NULL) {
struct net_device *m = qdisc_dev(q); struct net_device *m = qdisc_dev(q);
if (m) { if (m) {
dat->m->slaves = sch; spin_lock_bh(&dat->m->slaves_lock);
rcu_assign_pointer(dat->m->slaves, sch);
spin_unlock_bh(&dat->m->slaves_lock);
netif_wake_queue(m); netif_wake_queue(m);
} }
} else { } else {
@ -132,34 +135,49 @@ teql_destroy(struct Qdisc *sch)
struct Qdisc *q, *prev; struct Qdisc *q, *prev;
struct teql_sched_data *dat = qdisc_priv(sch); struct teql_sched_data *dat = qdisc_priv(sch);
struct teql_master *master = dat->m; struct teql_master *master = dat->m;
struct netdev_queue *txq = NULL;
bool reset_master_queue = false;
if (!master) if (!master)
return; return;
prev = master->slaves; spin_lock_bh(&master->slaves_lock);
prev = rcu_dereference_protected(master->slaves,
lockdep_is_held(&master->slaves_lock));
if (prev) { if (prev) {
do { do {
q = NEXT_SLAVE(prev); struct Qdisc *head, *next;
if (q == sch) {
NEXT_SLAVE(prev) = NEXT_SLAVE(q);
if (q == master->slaves) {
master->slaves = NEXT_SLAVE(q);
if (q == master->slaves) {
struct netdev_queue *txq;
txq = netdev_get_tx_queue(master->dev, 0); q = rcu_dereference_protected(NEXT_SLAVE(prev),
master->slaves = NULL; lockdep_is_held(&master->slaves_lock));
if (q != sch) {
dev_reset_queue(master->dev, prev = q;
txq, NULL); continue;
}
}
skb_queue_purge(&dat->q);
break;
} }
} while ((prev = q) != master->slaves); next = rcu_dereference_protected(NEXT_SLAVE(q),
lockdep_is_held(&master->slaves_lock));
rcu_assign_pointer(NEXT_SLAVE(prev), next);
head = rcu_dereference_protected(master->slaves,
lockdep_is_held(&master->slaves_lock));
if (q == head) {
rcu_assign_pointer(master->slaves, next);
if (q == next) {
txq = netdev_get_tx_queue(master->dev, 0);
rcu_assign_pointer(master->slaves, NULL);
reset_master_queue = true;
}
}
skb_queue_purge(&dat->q);
break;
} while (prev != rcu_dereference_protected(master->slaves,
lockdep_is_held(&master->slaves_lock)));
} }
spin_unlock_bh(&master->slaves_lock);
if (reset_master_queue)
dev_reset_queue(master->dev, txq, NULL);
} }
static int teql_qdisc_init(struct Qdisc *sch, struct nlattr *opt, static int teql_qdisc_init(struct Qdisc *sch, struct nlattr *opt,
@ -168,6 +186,7 @@ static int teql_qdisc_init(struct Qdisc *sch, struct nlattr *opt,
struct net_device *dev = qdisc_dev(sch); struct net_device *dev = qdisc_dev(sch);
struct teql_master *m = (struct teql_master *)sch->ops; struct teql_master *m = (struct teql_master *)sch->ops;
struct teql_sched_data *q = qdisc_priv(sch); struct teql_sched_data *q = qdisc_priv(sch);
struct Qdisc *first;
if (dev->hard_header_len > m->dev->hard_header_len) if (dev->hard_header_len > m->dev->hard_header_len)
return -EINVAL; return -EINVAL;
@ -184,7 +203,9 @@ static int teql_qdisc_init(struct Qdisc *sch, struct nlattr *opt,
skb_queue_head_init(&q->q); skb_queue_head_init(&q->q);
if (m->slaves) { spin_lock_bh(&m->slaves_lock);
first = rcu_dereference_protected(m->slaves, lockdep_is_held(&m->slaves_lock));
if (first) {
if (m->dev->flags & IFF_UP) { if (m->dev->flags & IFF_UP) {
if ((m->dev->flags & IFF_POINTOPOINT && if ((m->dev->flags & IFF_POINTOPOINT &&
!(dev->flags & IFF_POINTOPOINT)) || !(dev->flags & IFF_POINTOPOINT)) ||
@ -192,8 +213,10 @@ static int teql_qdisc_init(struct Qdisc *sch, struct nlattr *opt,
!(dev->flags & IFF_BROADCAST)) || !(dev->flags & IFF_BROADCAST)) ||
(m->dev->flags & IFF_MULTICAST && (m->dev->flags & IFF_MULTICAST &&
!(dev->flags & IFF_MULTICAST)) || !(dev->flags & IFF_MULTICAST)) ||
dev->mtu < m->dev->mtu) dev->mtu < m->dev->mtu) {
spin_unlock_bh(&m->slaves_lock);
return -EINVAL; return -EINVAL;
}
} else { } else {
if (!(dev->flags&IFF_POINTOPOINT)) if (!(dev->flags&IFF_POINTOPOINT))
m->dev->flags &= ~IFF_POINTOPOINT; m->dev->flags &= ~IFF_POINTOPOINT;
@ -204,14 +227,17 @@ static int teql_qdisc_init(struct Qdisc *sch, struct nlattr *opt,
if (dev->mtu < m->dev->mtu) if (dev->mtu < m->dev->mtu)
m->dev->mtu = dev->mtu; m->dev->mtu = dev->mtu;
} }
q->next = NEXT_SLAVE(m->slaves); rcu_assign_pointer(q->next,
NEXT_SLAVE(m->slaves) = sch; rcu_dereference_protected(NEXT_SLAVE(first),
lockdep_is_held(&m->slaves_lock)));
rcu_assign_pointer(NEXT_SLAVE(first), sch);
} else { } else {
q->next = sch; rcu_assign_pointer(q->next, sch);
m->slaves = sch; rcu_assign_pointer(m->slaves, sch);
m->dev->mtu = dev->mtu; m->dev->mtu = dev->mtu;
m->dev->flags = (m->dev->flags&~FMASK)|(dev->flags&FMASK); m->dev->flags = (m->dev->flags&~FMASK)|(dev->flags&FMASK);
} }
spin_unlock_bh(&m->slaves_lock);
return 0; return 0;
} }
@ -285,7 +311,9 @@ static netdev_tx_t teql_master_xmit(struct sk_buff *skb, struct net_device *dev)
int subq = skb_get_queue_mapping(skb); int subq = skb_get_queue_mapping(skb);
struct sk_buff *skb_res = NULL; struct sk_buff *skb_res = NULL;
start = master->slaves; rcu_read_lock_bh();
start = rcu_dereference_bh(master->slaves);
restart: restart:
nores = 0; nores = 0;
@ -317,10 +345,17 @@ static netdev_tx_t teql_master_xmit(struct sk_buff *skb, struct net_device *dev)
netdev_start_xmit(skb, slave, slave_txq, false) == netdev_start_xmit(skb, slave, slave_txq, false) ==
NETDEV_TX_OK) { NETDEV_TX_OK) {
__netif_tx_unlock(slave_txq); __netif_tx_unlock(slave_txq);
master->slaves = NEXT_SLAVE(q); spin_lock_bh(&master->slaves_lock);
if (rcu_dereference_protected(master->slaves,
lockdep_is_held(&master->slaves_lock)) == q)
rcu_assign_pointer(master->slaves,
rcu_dereference_protected(NEXT_SLAVE(q),
lockdep_is_held(&master->slaves_lock)));
spin_unlock_bh(&master->slaves_lock);
netif_wake_queue(dev); netif_wake_queue(dev);
master->tx_packets++; master->tx_packets++;
master->tx_bytes += length; master->tx_bytes += length;
rcu_read_unlock_bh();
return NETDEV_TX_OK; return NETDEV_TX_OK;
} }
__netif_tx_unlock(slave_txq); __netif_tx_unlock(slave_txq);
@ -329,14 +364,21 @@ static netdev_tx_t teql_master_xmit(struct sk_buff *skb, struct net_device *dev)
busy = 1; busy = 1;
break; break;
case 1: case 1:
master->slaves = NEXT_SLAVE(q); spin_lock_bh(&master->slaves_lock);
if (rcu_dereference_protected(master->slaves,
lockdep_is_held(&master->slaves_lock)) == q)
rcu_assign_pointer(master->slaves,
rcu_dereference_protected(NEXT_SLAVE(q),
lockdep_is_held(&master->slaves_lock)));
spin_unlock_bh(&master->slaves_lock);
rcu_read_unlock_bh();
return NETDEV_TX_OK; return NETDEV_TX_OK;
default: default:
nores = 1; nores = 1;
break; break;
} }
__skb_pull(skb, skb_network_offset(skb)); __skb_pull(skb, skb_network_offset(skb));
} while ((q = NEXT_SLAVE(q)) != start); } while ((q = rcu_dereference_bh(NEXT_SLAVE(q))) != start);
if (nores && skb_res == NULL) { if (nores && skb_res == NULL) {
skb_res = skb; skb_res = skb;
@ -345,29 +387,32 @@ static netdev_tx_t teql_master_xmit(struct sk_buff *skb, struct net_device *dev)
if (busy) { if (busy) {
netif_stop_queue(dev); netif_stop_queue(dev);
rcu_read_unlock_bh();
return NETDEV_TX_BUSY; return NETDEV_TX_BUSY;
} }
master->tx_errors++; master->tx_errors++;
drop: drop:
master->tx_dropped++; master->tx_dropped++;
rcu_read_unlock_bh();
dev_kfree_skb(skb); dev_kfree_skb(skb);
return NETDEV_TX_OK; return NETDEV_TX_OK;
} }
static int teql_master_open(struct net_device *dev) static int teql_master_open(struct net_device *dev)
{ {
struct Qdisc *q; struct Qdisc *q, *first;
struct teql_master *m = netdev_priv(dev); struct teql_master *m = netdev_priv(dev);
int mtu = 0xFFFE; int mtu = 0xFFFE;
unsigned int flags = IFF_NOARP | IFF_MULTICAST; unsigned int flags = IFF_NOARP | IFF_MULTICAST;
if (m->slaves == NULL) first = rtnl_dereference(m->slaves);
if (!first)
return -EUNATCH; return -EUNATCH;
flags = FMASK; flags = FMASK;
q = m->slaves; q = first;
do { do {
struct net_device *slave = qdisc_dev(q); struct net_device *slave = qdisc_dev(q);
@ -389,7 +434,7 @@ static int teql_master_open(struct net_device *dev)
flags &= ~IFF_BROADCAST; flags &= ~IFF_BROADCAST;
if (!(slave->flags&IFF_MULTICAST)) if (!(slave->flags&IFF_MULTICAST))
flags &= ~IFF_MULTICAST; flags &= ~IFF_MULTICAST;
} while ((q = NEXT_SLAVE(q)) != m->slaves); } while ((q = rtnl_dereference(NEXT_SLAVE(q))) != first);
m->dev->mtu = mtu; m->dev->mtu = mtu;
m->dev->flags = (m->dev->flags&~FMASK) | flags; m->dev->flags = (m->dev->flags&~FMASK) | flags;
@ -417,14 +462,15 @@ static void teql_master_stats64(struct net_device *dev,
static int teql_master_mtu(struct net_device *dev, int new_mtu) static int teql_master_mtu(struct net_device *dev, int new_mtu)
{ {
struct teql_master *m = netdev_priv(dev); struct teql_master *m = netdev_priv(dev);
struct Qdisc *q; struct Qdisc *q, *first;
q = m->slaves; first = rtnl_dereference(m->slaves);
q = first;
if (q) { if (q) {
do { do {
if (new_mtu > qdisc_dev(q)->mtu) if (new_mtu > qdisc_dev(q)->mtu)
return -EINVAL; return -EINVAL;
} while ((q = NEXT_SLAVE(q)) != m->slaves); } while ((q = rtnl_dereference(NEXT_SLAVE(q))) != first);
} }
WRITE_ONCE(dev->mtu, new_mtu); WRITE_ONCE(dev->mtu, new_mtu);
@ -444,6 +490,7 @@ static __init void teql_master_setup(struct net_device *dev)
struct teql_master *master = netdev_priv(dev); struct teql_master *master = netdev_priv(dev);
struct Qdisc_ops *ops = &master->qops; struct Qdisc_ops *ops = &master->qops;
spin_lock_init(&master->slaves_lock);
master->dev = dev; master->dev = dev;
ops->priv_size = sizeof(struct teql_sched_data); ops->priv_size = sizeof(struct teql_sched_data);