mirror of
https://github.com/torvalds/linux.git
synced 2026-11-07 06:17:02 +01:00
net/sched: sch_teql: fix slaves_lock IRQ deadlock
This is a followup of commite5b811fe79("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: commit6301f6a34e("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:
parent
d2bfe74a8f
commit
8b2043279d
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user