[PATCH v2 2/2] wifi: rtw89: support raw frame injection on standalone monitor interface

From: ph4ntom-rev

Date: Tue Nov 03 2026 - 11:00:14 EST


When injecting raw 802.11 Radiotap frames into a standalone monitor
interface (NL80211_IFTYPE_MONITOR brought up via 'iw set type monitor'),
net/mac80211/tx.c (ieee80211_monitor_start_xmit) transmits the frame
without an operational station context, passing info->control.vif = NULL.
Evaluating vif_to_rtwvif(vif) unconditionally in rtw89_ops_tx() triggers
a kernel NULL pointer dereference OOPS.

Furthermore, on hardware scheduler execution:
1. In rtw89_core_tx_update_injection(), handle rates[0].idx < 0 (omitted
bitrate in Radiotap) by changing idx to signed s8, falling back to
OFDM 6 Mbps (RTW89_HW_RATE_OFDM6) with use_rate and dis_data_fb set,
and bounding retries when IEEE80211_TX_CTL_NO_ACK is set. Always enable
en_wd_info for injected frames so txwd_info is built into the
descriptor.
2. In rtw89_vif_type_mapping(), configure NL80211_IFTYPE_MONITOR with
RTW89_NET_TYPE_NO_LINK and RTW89_SELF_ROLE_AP.
In rtw89_ops_add_interface(), initialize default CMAC/DMAC tables via
rtw89_chip_h2c_default_*mac_tbl, issue rtw89_fw_h2c_join_info(), and
unpause the MACID via rtw89_mac_unpause_macid() across all 4 MACID
sleep banks.
3. Protect pure_monitor_mode_vif with RCU: annotate with __rcu in core.h,
guard rtw89_ops_tx() with rcu_read_lock(), and place rcu_assign_pointer
and synchronize_rcu() at the very top of rtw89_ops_remove_interface()
prior to teardown of links, CAM, and MACID resources to completely
prevent Use-After-Free conditions. Disassociate firmware join state
via rtw89_fw_h2c_join_info(..., true) on interface removal, and clear
pure_monitor_mode_vif if a non-monitor interface is added.

Signed-off-by: ph4ntom-rev <owner@xxxxxxxxxxxxxx>
---
v1 -> v2:
- Use struct rtw89_vif_link * in rtw89_chip_h2c_default_*mac_tbl() and
rtw89_fw_h2c_join_info() to match modern MLO link abstractions.
- Protect pure_monitor_mode_vif with RCU, wrap TX path in rcu_read_lock(),
and use rtwvif_to_vif() helper instead of nonexistent struct member.
- In rtw89_ops_remove_interface(), move rcu_assign_pointer(..., NULL) and
synchronize_rcu() to the very top to prevent Use-After-Free during teardown.
- Issue rtw89_fw_h2c_join_info(..., true) on monitor interface removal to
cleanly disassociate firmware state.
- Reset pure_monitor_mode_vif if another non-monitor interface is added.
- Encapsulate sleep mask unpausing into rtw89_mac_unpause_macid() in mac.c
covering all 4 register banks (R_AX_MACID_SLEEP_0..3) guarded by
RTW89_CHIP_AX.
- Use lockdep_assert_wiphy and rcu_dereference_protected() in
rtw89_chip_rfk_channel_for_pure_mon_vif() to avoid context switch
warnings in sleeping RF calibration routines.
- Change rate index type to signed s8 in rtw89_core_tx_update_injection(),
fall back to OFDM 6 Mbps, and enable en_wd_info for descriptor building.
drivers/net/wireless/realtek/rtw89/core.c | 24 ++++++++++--
drivers/net/wireless/realtek/rtw89/core.h | 2 +-
drivers/net/wireless/realtek/rtw89/mac.c | 11 ++++++
drivers/net/wireless/realtek/rtw89/mac.h | 1 +
drivers/net/wireless/realtek/rtw89/mac80211.c | 39 +++++++++++++++++--
5 files changed, 69 insertions(+), 8 deletions(-)

diff --git a/drivers/net/wireless/realtek/rtw89/core.c b/drivers/net/wireless/realtek/rtw89/core.c
index 486c177..af31b94 100644
--- a/drivers/net/wireless/realtek/rtw89/core.c
+++ b/drivers/net/wireless/realtek/rtw89/core.c
@@ -474,7 +474,7 @@ void rtw89_chip_rfk_channel(struct rtw89_dev *rtwdev,
struct rtw89_vif_link *rtwvif_link)
{
const struct rtw89_chip_info *chip = rtwdev->chip;
- bool mon = !!rtwdev->pure_monitor_mode_vif;
+ bool mon = !!rcu_access_pointer(rtwdev->pure_monitor_mode_vif);
bool prehdl_link = false;

if (chip->chip_gen != RTW89_CHIP_AX &&
@@ -499,9 +499,13 @@ void rtw89_chip_rfk_channel(struct rtw89_dev *rtwdev,
static void rtw89_chip_rfk_channel_for_pure_mon_vif(struct rtw89_dev *rtwdev,
enum rtw89_phy_idx phy_idx)
{
- struct rtw89_vif *rtwvif = rtwdev->pure_monitor_mode_vif;
+ struct rtw89_vif *rtwvif;
struct rtw89_vif_link *rtwvif_link;

+ lockdep_assert_wiphy(rtwdev->hw->wiphy);
+
+ rtwvif = rcu_dereference_protected(rtwdev->pure_monitor_mode_vif,
+ lockdep_is_held(&rtwdev->hw->wiphy->mtx));
if (!rtwvif)
return;

@@ -1171,9 +1175,21 @@ static void rtw89_core_tx_update_injection(struct rtw89_dev *rtwdev,
enum mac80211_rate_control_flags flags = info->control.rates[0].flags;
struct rtw89_tx_desc_info *desc_info = &tx_req->desc_info;
const struct rtw89_chan *chan;
- u8 idx = info->control.rates[0].idx;
+ s8 idx = info->control.rates[0].idx;
u8 nss, mcs;

+ desc_info->en_wd_info = true;
+ if (idx < 0) {
+ desc_info->data_rate = RTW89_HW_RATE_OFDM6;
+ desc_info->use_rate = true;
+ desc_info->dis_data_fb = true;
+ if (info->flags & IEEE80211_TX_CTL_NO_ACK) {
+ desc_info->tx_cnt_lmt_en = true;
+ desc_info->tx_cnt_lmt = 1;
+ }
+ return;
+ }
+
desc_info->use_rate = true;
desc_info->dis_data_fb = true;

@@ -5006,6 +5022,8 @@ void rtw89_vif_type_mapping(struct rtw89_vif_link *rtwvif_link, bool assoc)
rtwvif_link->addr_cam.sec_ent_mode = RTW89_ADDR_CAM_SEC_NORMAL;
break;
case NL80211_IFTYPE_MONITOR:
+ rtwvif_link->net_type = RTW89_NET_TYPE_NO_LINK;
+ rtwvif_link->self_role = RTW89_SELF_ROLE_AP;
break;
default:
WARN_ON(1);
diff --git a/drivers/net/wireless/realtek/rtw89/core.h b/drivers/net/wireless/realtek/rtw89/core.h
index 05adf20..e844138 100644
--- a/drivers/net/wireless/realtek/rtw89/core.h
+++ b/drivers/net/wireless/realtek/rtw89/core.h
@@ -6318,7 +6318,7 @@ struct rtw89_dev {
int napi_budget_countdown;

struct rtw89_debugfs *debugfs;
- struct rtw89_vif *pure_monitor_mode_vif;
+ struct rtw89_vif __rcu *pure_monitor_mode_vif;

/* HCI related data, keep last */
u8 priv[] __aligned(sizeof(void *));
diff --git a/drivers/net/wireless/realtek/rtw89/mac.c b/drivers/net/wireless/realtek/rtw89/mac.c
index 54aad37..58b0fe0 100644
--- a/drivers/net/wireless/realtek/rtw89/mac.c
+++ b/drivers/net/wireless/realtek/rtw89/mac.c
@@ -6240,6 +6240,17 @@ void rtw89_mac_flush_txq(struct rtw89_dev *rtwdev, u32 queues, bool drop)
rtw89_info(rtwdev, "timed out to flush queues\n");
}

+void rtw89_mac_unpause_macid(struct rtw89_dev *rtwdev, u8 mac_id)
+{
+ u8 grp = mac_id >> 5;
+ u8 shift = mac_id & 0x1f;
+
+ rtw89_fw_h2c_macid_pause(rtwdev, shift, grp, false);
+
+ if (rtwdev->chip->chip_gen == RTW89_CHIP_AX && grp < 4)
+ rtw89_write32_clr(rtwdev, R_AX_MACID_SLEEP_0 + grp * 4, BIT(shift));
+}
+
int rtw89_mac_coex_init(struct rtw89_dev *rtwdev, const struct rtw89_mac_ax_coex *coex)
{
enum rtw89_core_chip_id chip_id = rtwdev->chip->chip_id;
diff --git a/drivers/net/wireless/realtek/rtw89/mac.h b/drivers/net/wireless/realtek/rtw89/mac.h
index 9db9ae2..587bcb6 100644
--- a/drivers/net/wireless/realtek/rtw89/mac.h
+++ b/drivers/net/wireless/realtek/rtw89/mac.h
@@ -1432,6 +1432,7 @@ void rtw89_mac_set_edcca_mode_bands(struct rtw89_dev *rtwdev, bool normal)
void rtw89_mac_set_rx_fltr(struct rtw89_dev *rtwdev, u8 mac_idx, u32 rx_fltr);
void rtw89_mac_update_rts_threshold(struct rtw89_dev *rtwdev);
void rtw89_mac_flush_txq(struct rtw89_dev *rtwdev, u32 queues, bool drop);
+void rtw89_mac_unpause_macid(struct rtw89_dev *rtwdev, u8 mac_id);
int rtw89_mac_coex_init(struct rtw89_dev *rtwdev, const struct rtw89_mac_ax_coex *coex);
int rtw89_mac_coex_init_v1(struct rtw89_dev *rtwdev,
const struct rtw89_mac_ax_coex *coex);
diff --git a/drivers/net/wireless/realtek/rtw89/mac80211.c b/drivers/net/wireless/realtek/rtw89/mac80211.c
index 33a6598..3baa627 100644
--- a/drivers/net/wireless/realtek/rtw89/mac80211.c
+++ b/drivers/net/wireless/realtek/rtw89/mac80211.c
@@ -23,16 +23,30 @@ static void rtw89_ops_tx(struct ieee80211_hw *hw,
struct rtw89_dev *rtwdev = hw->priv;
struct ieee80211_tx_info *info = IEEE80211_SKB_CB(skb);
struct ieee80211_vif *vif = info->control.vif;
- struct rtw89_vif *rtwvif = vif_to_rtwvif(vif);
+ struct rtw89_vif *rtwvif;
struct ieee80211_sta *sta = control->sta;
u32 flags = IEEE80211_SKB_CB(skb)->flags;
int ret, qsel;

+ rcu_read_lock();
+ if (!vif) {
+ rtwvif = rcu_dereference(rtwdev->pure_monitor_mode_vif);
+ if (!rtwvif) {
+ rcu_read_unlock();
+ ieee80211_free_txskb(hw, skb);
+ return;
+ }
+ vif = rtwvif_to_vif(rtwvif);
+ } else {
+ rtwvif = vif_to_rtwvif(vif);
+ }
+
if (rtwvif->offchan && !(flags & IEEE80211_TX_CTL_TX_OFFCHAN) && sta) {
struct rtw89_sta *rtwsta = sta_to_rtwsta(sta);

rtw89_debug(rtwdev, RTW89_DBG_TXRX, "ops_tx during offchan\n");
skb_queue_tail(&rtwsta->roc_queue, skb);
+ rcu_read_unlock();
return;
}

@@ -40,9 +54,11 @@ static void rtw89_ops_tx(struct ieee80211_hw *hw,
if (ret) {
rtw89_err(rtwdev, "failed to transmit skb: %d\n", ret);
ieee80211_free_txskb(hw, skb);
+ rcu_read_unlock();
return;
}
rtw89_core_tx_kick_off(rtwdev, qsel);
+ rcu_read_unlock();
}

static void rtw89_ops_wake_tx_queue(struct ieee80211_hw *hw,
@@ -221,8 +237,16 @@ static int rtw89_ops_add_interface(struct ieee80211_hw *hw,
if (ret)
goto unset_link;

- rtwdev->pure_monitor_mode_vif = vif->type == NL80211_IFTYPE_MONITOR ?
- rtwvif : NULL;
+ if (vif->type == NL80211_IFTYPE_MONITOR) {
+ rtw89_chip_h2c_default_cmac_tbl(rtwdev, rtwvif_link, NULL);
+ rtw89_chip_h2c_default_dmac_tbl(rtwdev, rtwvif_link, NULL);
+ rtw89_fw_h2c_join_info(rtwdev, rtwvif_link, NULL, false);
+ rtw89_mac_unpause_macid(rtwdev, rtwvif_link->mac_id);
+ rcu_assign_pointer(rtwdev->pure_monitor_mode_vif, rtwvif);
+ } else if (rcu_access_pointer(rtwdev->pure_monitor_mode_vif)) {
+ rcu_assign_pointer(rtwdev->pure_monitor_mode_vif, NULL);
+ synchronize_rcu();
+ }
rtw89_recalc_lps(rtwdev);
return 0;

@@ -248,6 +272,11 @@ static void rtw89_ops_remove_interface(struct ieee80211_hw *hw,

lockdep_assert_wiphy(hw->wiphy);

+ if (vif->type == NL80211_IFTYPE_MONITOR) {
+ rcu_assign_pointer(rtwdev->pure_monitor_mode_vif, NULL);
+ synchronize_rcu();
+ }
+
rtw89_debug(rtwdev, RTW89_DBG_STATE, "remove vif %pM type %d p2p %d\n",
vif->addr, vif->type, vif->p2p);

@@ -261,6 +290,9 @@ static void rtw89_ops_remove_interface(struct ieee80211_hw *hw,
goto bottom;
}

+ if (vif->type == NL80211_IFTYPE_MONITOR)
+ rtw89_fw_h2c_join_info(rtwdev, rtwvif_link, NULL, true);
+
__rtw89_ops_remove_iface_link(rtwdev, rtwvif_link);

rtw89_vif_unset_link(rtwvif, RTW89_VIF_IDLE_LINK_ID);
@@ -270,7 +302,6 @@ bottom:
rtw89_core_release_bit_map(rtwdev->hw_port, port);
rtw89_release_mac_id(rtwdev, macid);

- rtwdev->pure_monitor_mode_vif = NULL;

rtw89_recalc_lps(rtwdev);
rtw89_enter_ips_by_hwflags(rtwdev);
--
2.53.0