[PATCH wireless] wifi: mt76: mt7921: fix deadlock on 6 GHz association

Mikhail Gavrilov mikhail.v.gavrilov at gmail.com
Sat Aug 15 03:28:06 PDT 2026


mt7921_mcu_regd_update() acquires &dev->mt76.mutex, but two of its
callers already hold it: mt76_sta_state() takes the mutex and calls into
the driver with it held, on both the add and the remove path.

  mt76_sta_state                          <- takes &dev->mutex
    mt7921_mac_sta_add
      mt7921_regd_set_6ghz_power_type(vif, true)
        mt7921_mcu_regd_update            <- takes &dev->mutex again

  mt76_sta_state                          <- takes &dev->mutex
    mt7921_mac_sta_remove
      mt7921_regd_set_6ghz_power_type(vif, false)
        mt7921_mcu_regd_update            <- takes &dev->mutex again

Both acquisitions are of the same lock instance, so this is a hard
self-deadlock rather than a missing nesting annotation:

  WARNING: possible recursive locking detected
  wpa_supplicant/5319 is trying to acquire lock:
  ffff88879cd94050 (&dev->mutex#3){+.+.}-{4:4}, at: mt7921_mcu_regd_update+0xc0/0x7a0 [mt7921_common]
  but task is already holding lock:
  ffff88879cd94050 (&dev->mutex#3){+.+.}-{4:4}, at: mt76_sta_state+0x2d4/0xb30 [mt76]
  Call Trace:
   <TASK>
   mt7921_mcu_regd_update+0xc0/0x7a0 [mt7921_common]
   mt7921_regd_set_6ghz_power_type+0x25b/0x2e0 [mt7921_common]
   mt7921_mac_sta_add+0x33f/0x480 [mt7921_common]
   mt76_sta_state+0x335/0xb30 [mt76]
   drv_sta_state+0x284/0x740 [mac80211]
   sta_info_insert_finish+0x4dc/0x1070 [mac80211]
   ieee80211_prep_connection+0xaf3/0x1740 [mac80211]
   ieee80211_mgd_auth+0xcba/0x1a00 [mac80211]
   cfg80211_mlme_auth+0x47b/0xab0 [cfg80211]
   nl80211_authenticate+0xa34/0xdf0 [cfg80211]
   </TASK>

The kernel then says it outright:

  INFO: task wpa_supplicant:5319 is blocked on a mutex likely owned by
  task wpa_supplicant:5319.

The damage is not confined to Wi-Fi. wpa_supplicant blocks while holding
wiphy.mtx, NetworkManager then blocks on wiphy.mtx while holding rtnl,
and everything needing rtnl queues up behind it: rtnl_dumpit, ethtool
ioctls, cleanup_net. The machine is left with no working network stack
and needs sysrq to reboot. Since NetworkManager retries the saved
profile on every boot, an affected kernel stops reaching a usable state
at all once a 6 GHz profile exists.

Before commit e88098133ed4
("wifi: mt76: mt7921: refactor regulatory notifier flow") the function
was lock-free by contract and its callers supplied the lock: the sta_add
and sta_remove paths already held it, and mt7921_regd_notifier() took it
explicitly. That commit moved mt792x_mutex_acquire() inside the
function, which is what mt7921_pci_resume() needed - it had been calling
in with no lock at all - but it left the two mac80211 paths taking the
mutex twice.

That those paths run with the mutex held is not incidental: each ends
with a hand-rolled mt76_connac_power_save_sched(), the half of
mt792x_mutex_release() that the core's plain mutex_lock() does not
provide. The lock-free contract was deliberate.

Split the function: keep a lock-free __mt7921_mcu_regd_update() for
callers that already hold the mutex, and a thin locking wrapper for
those that do not. The regulatory notifier and the PCI resume path are
unchanged.

The deadlock is gated on the band. mt7921_regd_set_6ghz_power_type()
only issues the update when

	vif->bss_conf.chanreq.oper.chan->band == NL80211_BAND_6GHZ

so it takes a 6 GHz association to reach it, which is likely why this
survived seven -rc rounds unreported.

Fixes: e88098133ed4 ("wifi: mt76: mt7921: refactor regulatory notifier flow")
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov at gmail.com>
---

#regzbot introduced: e88098133ed4

Not addressed here: the "if (!dev->regd_change) goto err" gate combined
with clearing regd_change on exit makes the sta_add/sta_remove call a
no-op in the common case. That call site may want removing rather than
relocking, but that is a behavioural decision for you.

Tested on an MT7922 (mt7921e) on v7.2-rc7 with a lockdep and UBSAN
build: association with a 6 GHz AP completes (channel 37, 6135 MHz,
160 MHz), roaming between a 5 GHz and a 6 GHz BSS exercises both the
sta_add and the sta_remove call site, "iw reg set NL" and back still
reaches the regulatory notifier, and a deep suspend/resume cycle
reconnects to the 6 GHz BSS. dmesg is clean. v7.1 is unaffected.

 .../net/wireless/mediatek/mt76/mt7921/main.c  |  2 +-
 .../net/wireless/mediatek/mt76/mt7921/regd.c  | 20 +++++++++++++++----
 .../net/wireless/mediatek/mt76/mt7921/regd.h  |  2 ++
 3 files changed, 19 insertions(+), 5 deletions(-)

diff --git a/drivers/net/wireless/mediatek/mt76/mt7921/main.c b/drivers/net/wireless/mediatek/mt76/mt7921/main.c
index 3480205d5fb9..68a059504e83 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7921/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7921/main.c
@@ -802,7 +802,7 @@ mt7921_regd_set_6ghz_power_type(struct ieee80211_vif *vif, bool is_add)
 
 out:
 	if (vif->bss_conf.chanreq.oper.chan->band == NL80211_BAND_6GHZ)
-		mt7921_mcu_regd_update(dev, dev->mt76.alpha2, dev->country_ie_env);
+		__mt7921_mcu_regd_update(dev, dev->mt76.alpha2, dev->country_ie_env);
 }
 
 int mt7921_mac_sta_add(struct mt76_dev *mdev, struct ieee80211_vif *vif,
diff --git a/drivers/net/wireless/mediatek/mt76/mt7921/regd.c b/drivers/net/wireless/mediatek/mt76/mt7921/regd.c
index c0e2b48a50bf..43193c436ddc 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7921/regd.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7921/regd.c
@@ -71,17 +71,18 @@ mt7921_regd_channel_update(struct wiphy *wiphy, struct mt792x_dev *dev)
 	}
 }
 
-int mt7921_mcu_regd_update(struct mt792x_dev *dev, u8 *alpha2,
-			   enum environment_cap country_ie_env)
+int __mt7921_mcu_regd_update(struct mt792x_dev *dev, u8 *alpha2,
+			     enum environment_cap country_ie_env)
 {
 	struct mt76_dev *mdev = &dev->mt76;
 	struct ieee80211_hw *hw = mdev->hw;
 	struct wiphy *wiphy = hw->wiphy;
 	int ret = 0;
 
+	lockdep_assert_held(&mdev->mutex);
+
 	dev->regd_in_progress = true;
 
-	mt792x_mutex_acquire(dev);
 	if (!dev->regd_change)
 		goto err;
 
@@ -100,13 +101,24 @@ int mt7921_mcu_regd_update(struct mt792x_dev *dev, u8 *alpha2,
 		goto err;
 
 err:
-	mt792x_mutex_release(dev);
 	dev->regd_change = false;
 	dev->regd_in_progress = false;
 	wake_up(&dev->wait);
 
 	return ret;
 }
+
+int mt7921_mcu_regd_update(struct mt792x_dev *dev, u8 *alpha2,
+			   enum environment_cap country_ie_env)
+{
+	int ret;
+
+	mt792x_mutex_acquire(dev);
+	ret = __mt7921_mcu_regd_update(dev, alpha2, country_ie_env);
+	mt792x_mutex_release(dev);
+
+	return ret;
+}
 EXPORT_SYMBOL_GPL(mt7921_mcu_regd_update);
 
 void mt7921_regd_notifier(struct wiphy *wiphy,
diff --git a/drivers/net/wireless/mediatek/mt76/mt7921/regd.h b/drivers/net/wireless/mediatek/mt76/mt7921/regd.h
index 571f31629e9e..2ea12d3861fb 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7921/regd.h
+++ b/drivers/net/wireless/mediatek/mt76/mt7921/regd.h
@@ -8,6 +8,8 @@ struct mt792x_dev;
 struct wiphy;
 struct regulatory_request;
 
+int __mt7921_mcu_regd_update(struct mt792x_dev *dev, u8 *alpha2,
+			     enum environment_cap country_ie_env);
 int mt7921_mcu_regd_update(struct mt792x_dev *dev, u8 *alpha2,
 			   enum environment_cap country_ie_env);
 void mt7921_regd_notifier(struct wiphy *wiphy,
-- 
2.55.0




More information about the Linux-mediatek mailing list