net/sched: sch_teql: fix slaves_lock IRQ deadlock

This is a followup of commit e5b811fe79 ("net/sched: sch_teql:
Introduce slaves_lock to avoid race condition and UAF") as reported by
Sashiko.

The issue: master->slaves_lock serializes the master's ring of slave
qdiscs.  The control paths -- teql_dequeue(), teql_destroy() and
teql_qdisc_init() -- take it with spin_lock_bh(), while
teql_master_xmit() (the master's ndo_start_xmit) takes it with plain
spin_lock().

netpoll_send_skb() wraps the transmit in local_irq_save(), and
__netpoll_send_skb() asserts lockdep_assert_irqs_disabled(), so
teql_master_xmit() reached from netconsole always acquires slaves_lock
with hard IRQs disabled.  On PREEMPT_RT spinlock_t is an rtmutex, and
Documentation/locking/locktypes.rst names

	local_irq_disable();
	spin_lock(&lock);

as a sequence that breaks there because the rtmutex requires a fully
preemptible context.  This is reachable without any panic: the nbcon
printer thread calls netconsole's write_thread() callback on every
message.  The usual remedy -- folding the IRQ disable into the lock with
spin_lock_irqsave() -- is unavailable because the local_irq_save() is
netpoll's, not ours.

On non-RT the same asymmetry leaves slaves_lock acquirable with hard IRQs
disabled on one path and held with hard IRQs enabled on another.  That is
only exploitable during the panic console flush (netconsole is
CON_NBCON_ATOMIC_UNSAFE, so console_is_usable() admits its write_atomic()
only from nbcon_atomic_flush_unsafe(), which panic() alone calls), and
even then an IRQ-saving lock does not help if the holder oopsed inside the
section or was stopped by panic_other_cpus_shutdown().  So the non-RT half
is hardening; the PREEMPT_RT half is the bug.

The Fix: take slaves_lock as a raw spinlock with IRQs saved at every
site.  Since skb_queue_purge() takes the queue's spinlock_t and may not
nest inside a raw lock on PREEMPT_RT, move its call in teql_destroy()
outside the raw section; dat->q is the dying qdisc's own queue and was
never serialized by slaves_lock, so the move is behaviour-neutral.

The new critical sections contain only RCU pointer assignments and
netdev_get_tx_queue(); skb_queue_purge(), netif_wake_queue() and
dev_reset_queue() are all outside them.  teql_destroy()'s ring walk is
O(slaves), two pointer loads and a compare per iteration, so even a large
ring is microseconds with IRQs off.  Keeping the purge conditional rather
than dropping it outright is the conservative call for a backport.

Some context: commit 6301f6a34e ("net/sched: sch_teql: move
rcu_read_lock()/spin_lock() from _bh variants") hardened the parent
fix; together they addressed the master->slaves resurrection
use-after-free and the xmit-side BH warning but left the reverse
direction -- a hard IRQ re-entering xmit while a control path holds the
lock -- unaddressed.

Conditions to recreate the bug:
- CONFIG_PREEMPT_RT, CONFIG_NET_SCH_TEQL, CONFIG_DUMMY and
  CONFIG_NETCONSOLE.
- Run `ip link add dev teqlslave0 type dummy`, bring teqlslave0 up, attach
  it with `tc qdisc add dev teqlslave0 root teql0`, and bring teql0 up.
- Configure a netconsole target on teql0 with explicit local and remote
  IP addresses and a remote MAC address, then emit a console message.
  The nbcon printer thread reaches teql_master_xmit() with hard IRQs
  disabled by netpoll; taking slaves_lock as a spinlock_t is invalid on
  PREEMPT_RT. No panic or concurrent qdisc change is required.
- Reachable by an admin with CAP_NET_ADMIN; teql devices exist only in the
  initial netns (no per-netns ops), so a user namespace does not suffice.

Fixes: e5b811fe79 ("net/sched: sch_teql: Introduce slaves_lock to avoid race condition and UAF")
Reported-by: Sashiko (gemini) <sashiko-bot@kernel.org>
Closes: https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260630150922.238714-1-jhs%40mojatatu.com
Link: https://lore.kernel.org/netdev/20260630150922.238714-1-jhs@mojatatu.com/
Cc: stable@vger.kernel.org
Signed-off-by: Jamal Hadi Salim <jhs@mojatatu.com>
Reviewed-by: Simon Horman <horms@kernel.org>
Link: https://patch.msgid.link/QDISC-Y0BY.v2.r0.20261024060625-1@mojatatu.com
Signed-off-by: Paolo Abeni <pabeni@redhat.com>
This commit is contained in:
Jamal Hadi Salim 2026-10-24 10:34:14 -04:00 committed by Paolo Abeni
parent d2bfe74a8f
commit 8b2043279d

View File

@ -55,7 +55,7 @@ struct teql_master {
struct Qdisc_ops qops;
struct net_device *dev;
struct Qdisc __rcu *slaves;
spinlock_t slaves_lock; /* serializes writes to ->slaves */
raw_spinlock_t slaves_lock; /* serializes writes to ->slaves */
struct list_head master_list;
unsigned long tx_bytes;
unsigned long tx_packets;
@ -94,6 +94,7 @@ teql_dequeue(struct Qdisc *sch)
{
struct teql_sched_data *dat = qdisc_priv(sch);
struct netdev_queue *dat_queue;
unsigned long flags;
struct sk_buff *skb;
struct Qdisc *q;
@ -104,9 +105,9 @@ teql_dequeue(struct Qdisc *sch)
if (skb == NULL) {
struct net_device *m = qdisc_dev(q);
if (m) {
spin_lock_bh(&dat->m->slaves_lock);
raw_spin_lock_irqsave(&dat->m->slaves_lock, flags);
rcu_assign_pointer(dat->m->slaves, sch);
spin_unlock_bh(&dat->m->slaves_lock);
raw_spin_unlock_irqrestore(&dat->m->slaves_lock, flags);
netif_wake_queue(m);
}
} else {
@ -139,11 +140,13 @@ teql_destroy(struct Qdisc *sch)
struct teql_master *master = dat->m;
struct netdev_queue *txq = NULL;
bool reset_master_queue = false;
unsigned long flags;
bool purge = false;
if (!master)
return;
spin_lock_bh(&master->slaves_lock);
raw_spin_lock_irqsave(&master->slaves_lock, flags);
prev = rcu_dereference_protected(master->slaves,
lockdep_is_held(&master->slaves_lock));
if (prev) {
@ -171,12 +174,18 @@ teql_destroy(struct Qdisc *sch)
reset_master_queue = true;
}
}
skb_queue_purge(&dat->q);
purge = true;
break;
} while (prev != rcu_dereference_protected(master->slaves,
lockdep_is_held(&master->slaves_lock)));
}
spin_unlock_bh(&master->slaves_lock);
raw_spin_unlock_irqrestore(&master->slaves_lock, flags);
/* skb_queue_purge() takes the queue's spinlock_t, which cannot nest
* inside a raw lock on PREEMPT_RT; purge after the raw section.
*/
if (purge)
skb_queue_purge(&dat->q);
if (reset_master_queue)
dev_reset_queue(master->dev, txq, NULL);
@ -189,6 +198,7 @@ static int teql_qdisc_init(struct Qdisc *sch, struct nlattr *opt,
struct teql_master *m = (struct teql_master *)sch->ops;
struct teql_sched_data *q = qdisc_priv(sch);
struct Qdisc *first;
unsigned long flags;
if (dev->hard_header_len > m->dev->hard_header_len)
return -EINVAL;
@ -205,7 +215,7 @@ static int teql_qdisc_init(struct Qdisc *sch, struct nlattr *opt,
skb_queue_head_init(&q->q);
spin_lock_bh(&m->slaves_lock);
raw_spin_lock_irqsave(&m->slaves_lock, flags);
first = rcu_dereference_protected(m->slaves, lockdep_is_held(&m->slaves_lock));
if (first) {
if (m->dev->flags & IFF_UP) {
@ -216,7 +226,7 @@ static int teql_qdisc_init(struct Qdisc *sch, struct nlattr *opt,
(m->dev->flags & IFF_MULTICAST &&
!(dev->flags & IFF_MULTICAST)) ||
dev->mtu < m->dev->mtu) {
spin_unlock_bh(&m->slaves_lock);
raw_spin_unlock_irqrestore(&m->slaves_lock, flags);
return -EINVAL;
}
} else {
@ -239,7 +249,7 @@ static int teql_qdisc_init(struct Qdisc *sch, struct nlattr *opt,
m->dev->mtu = dev->mtu;
m->dev->flags = (m->dev->flags&~FMASK)|(dev->flags&FMASK);
}
spin_unlock_bh(&m->slaves_lock);
raw_spin_unlock_irqrestore(&m->slaves_lock, flags);
return 0;
}
@ -315,11 +325,12 @@ static inline int teql_resolve(struct sk_buff *skb,
static netdev_tx_t teql_master_xmit(struct sk_buff *skb, struct net_device *dev)
{
struct teql_master *master = netdev_priv(dev);
struct Qdisc *start, *q;
int busy;
int nores;
int subq = skb_get_queue_mapping(skb);
struct sk_buff *skb_res = NULL;
struct Qdisc *start, *q;
unsigned long flags;
int nores;
int busy;
restart:
nores = 0;
@ -355,13 +366,13 @@ static netdev_tx_t teql_master_xmit(struct sk_buff *skb, struct net_device *dev)
netdev_start_xmit(skb, slave, slave_txq, false) ==
NETDEV_TX_OK) {
__netif_tx_unlock(slave_txq);
spin_lock(&master->slaves_lock);
raw_spin_lock_irqsave(&master->slaves_lock, flags);
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(&master->slaves_lock);
raw_spin_unlock_irqrestore(&master->slaves_lock, flags);
netif_wake_queue(dev);
master->tx_packets++;
master->tx_bytes += length;
@ -374,13 +385,13 @@ static netdev_tx_t teql_master_xmit(struct sk_buff *skb, struct net_device *dev)
busy = 1;
break;
case 1:
spin_lock(&master->slaves_lock);
raw_spin_lock_irqsave(&master->slaves_lock, flags);
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(&master->slaves_lock);
raw_spin_unlock_irqrestore(&master->slaves_lock, flags);
rcu_read_unlock();
return NETDEV_TX_OK;
default:
@ -502,7 +513,7 @@ static __init void teql_master_setup(struct net_device *dev)
struct teql_master *master = netdev_priv(dev);
struct Qdisc_ops *ops = &master->qops;
spin_lock_init(&master->slaves_lock);
raw_spin_lock_init(&master->slaves_lock);
master->dev = dev;
ops->priv_size = sizeof(struct teql_sched_data);