[PATCH] wifi: mac80211: restore monitor injection when coexisting with another VIF

Brite
2026-04-24T12:08:52Z

Monitor-mode packet injection is broken on drivers that implement
real channel context ops (mt76 and others) when the
monitor interface runs alongside another interface (typically AP).
The monitor VIF never gets a chanctx of its own in this case, so
ieee80211_monitor_start_xmit() finds vif.bss_conf.chanctx_conf ==
NULL and takes the fail_rcu path, silently dropping the skb. In
practice this breaks tooling like mdk4 and aireplay-ng on mt76
hardware, including airgeddon's evil-twin deauth flow, which runs
hostapd on an AP VIF and injects deauth frames from a coexisting
monitor VIF.

Earlier attempts on this thread addressed the same bug but had side
effects - notably full VM freezes during the airgeddon evil-twin flow,
reported by Óscar in the thread. This patch takes a different approach
and has not exhibited those side effects across the tested configurations.

Fix in two independent pieces:

1) Snapshot-based fallback. Maintain an RCU-published snapshot,
local->sole_chandef, of the single active cfg80211_chan_def
when exactly one non-transitional chanctx exists on the device,
and NULL otherwise (MCC, mid-swap, idle, allocation failure).
The snapshot is refreshed from four chanctx call sites
(new/free/assign/change) under wiphy->mtx, and consumed
lock-free by ieee80211_monitor_start_xmit() under rcu_read_lock().
The wrapper struct carries an rcu_head so stale snapshots retire
via kfree_rcu(). Fail-closed on ambiguous channel state rather
than injecting on a guess.

This restores AP+monitor coexistence injection on 2.4 GHz.

2) Surrogate sdata for the regulatory check on 5 GHz. Once the
snapshot supplies a chandef, sdata is still the monitor
interface (injection tools usually spoof the source MAC so the
earlier addr2 match does not reassign sdata). On 5 GHz channels
cfg80211_reg_can_beacon() then rejects the frame because
NL80211_IFTYPE_MONITOR cannot satisfy the regulatory requirements
for that band. In the coexistence scenario there is already a
non-monitor VIF on the same channel that is authorised to operate
there; locate a running non-monitor sdata with a matching chandef
(cfg80211_chandef_identical: band, width, both center freqs) and
use it for the regulatory check. An AP sdata satisfies the check,
so the frame goes out on the correct channel instead of being
dropped. If no such sdata exists the monitor interface is left in
place and the existing code paths apply.

Tested on mt7921u (mt76) usb with mdk4 and aireplay-ng deauth on
2.4 GHz and 5 GHz while co-running an AP on the same channel.
Tested on 6.18.12, 6.19.12 and 7.0.0-rc5

Cc: stable@vger.kernel.org
Fixes: 0a44dfc07074 ("wifi: mac80211: simplify non-chanctx drivers")
Reported-by: Óscar Alfonso Díaz <oscar.alfonso.diaz@gmail.com>
Closes: https://bugzilla.kernel.org/show_bug.cgi?id=218763
Link: https://github.com/morrownr/USB-WiFi/issues/682
Signed-off-by: Brite <brite.airgeddon@gmail.com>
---
net/mac80211/chan.c | 75 ++++++++++++++++++++++++++++++++++++++
net/mac80211/ieee80211_i.h | 17 +++++++++
net/mac80211/main.c | 7 ++++
net/mac80211/tx.c | 69 +++++++++++++++++++++++++++++++++--
4 files changed, 165 insertions(+), 3 deletions(-)

diff --git a/net/mac80211/chan.c b/net/mac80211/chan.c
index 05f45e66999b..9efab86f57d0 100644
--- a/net/mac80211/chan.c
+++ b/net/mac80211/chan.c
@@ -12,6 +12,61 @@
#include "driver-ops.h"
#include "rate.h"

+/**
+ * ieee80211_update_sole_chandef - refresh the sole-chanctx snapshot
+ * @local: the mac80211 device
+ *
+ * Walk chanctx_list. If exactly one non-transitional, valid chanctx
+ * is present, kmalloc a snapshot of its chandef and RCU-publish it on
+ * local->sole_chandef. If zero or more than one chanctx are active,
+ * publish NULL (fail-closed; injection disabled for MCC or idle).
+ *
+ * The prior snapshot is freed via kfree_rcu after all RCU readers that
+ * hold a reference to it complete.
+ *
+ * Context: Must be called with wiphy->mtx held.
+ * Always process context - GFP_KERNEL is safe and appropriate.
+ */
+void ieee80211_update_sole_chandef(struct ieee80211_local *local)
+{
+ struct ieee80211_chanctx *ctx, *found = NULL;
+ struct ieee80211_sole_chandef *snap = NULL;
+ struct ieee80211_sole_chandef *old;
+
+ lockdep_assert_wiphy(local->hw.wiphy);
+
+ list_for_each_entry(ctx, &local->chanctx_list, list) {
+ /*
+ * REPLACES_OTHER: this entry is the incoming side of a
+ * swap; the outgoing context is still live. Skip it to
+ * avoid counting a context that is not yet active.
+ */
+ if (ctx->replace_state == IEEE80211_CHANCTX_REPLACES_OTHER)
+ continue;
+ if (!cfg80211_chandef_valid(&ctx->conf.def))
+ continue;
+
+ if (found) {
+ /* MCC or unexpected multi-channel state. */
+ found = NULL;
+ break;
+ }
+ found = ctx;
+ }
+
+ if (found) {
+ snap = kmalloc(sizeof(*snap), GFP_KERNEL);
+ if (snap)
+ snap->def = found->conf.def;
+ /* alloc failure -> snap == NULL -> publish NULL below */
+ }
+
+ old = rcu_replace_pointer(local->sole_chandef, snap,
+ lockdep_is_held(&local->hw.wiphy->mtx));
+ if (old)
+ kfree_rcu(old, rcu_head);
+}
+
struct ieee80211_chanctx_user_iter {
struct ieee80211_chan_req *chanreq;
struct ieee80211_sub_if_data *sdata;
@@ -729,6 +784,9 @@ static void ieee80211_change_chanctx(struct ieee80211_local *local,
const struct ieee80211_chan_req *chanreq)
{
_ieee80211_change_chanctx(local, ctx, old_ctx, chanreq, NULL);
+
+ /* Hook 4/4: channel parameters changed; refresh snapshot */
+ ieee80211_update_sole_chandef(local);
}

/* Note: if successful, the returned chanctx is reserved for the link */
@@ -902,6 +960,13 @@ ieee80211_new_chanctx(struct ieee80211_local *local,
WARN_ON_ONCE(err && !local->in_reconfig);

list_add_rcu(&ctx->list, &local->chanctx_list);
+ /*
+ * Hook 1/4: new context is now on the list.
+ * Publish a fresh snapshot so monitor injection can use this
+ * channel immediately.
+ */
+ ieee80211_update_sole_chandef(local);
+
return ctx;
}

@@ -928,6 +993,13 @@ static void ieee80211_free_chanctx(struct ieee80211_local *local,
WARN_ON_ONCE(ieee80211_chanctx_refcount(local, ctx) != 0);

list_del_rcu(&ctx->list);
+ /*
+ * Hook 2/4: context is now off the list.
+ * Republish so that a context removed during AP teardown is no
+ * longer visible to the monitor injection fallback.
+ */
+ ieee80211_update_sole_chandef(local);
+
ieee80211_del_chanctx(local, ctx, skip_idle_recalc);
kfree_rcu(ctx, rcu_head);
}
@@ -1061,6 +1133,9 @@ static int ieee80211_assign_link_chanctx(struct ieee80211_link_data *link,
ieee80211_recalc_chanctx_min_def(local, new_ctx);
}

+ /* Hook 3/4: VIF assigned or unassigned; refresh snapshot */
+ ieee80211_update_sole_chandef(local);
+
if (conf) {
new_idle = false;
} else {
diff --git a/net/mac80211/ieee80211_i.h b/net/mac80211/ieee80211_i.h
index e60b814dd89e..14c412a18868 100644
--- a/net/mac80211/ieee80211_i.h
+++ b/net/mac80211/ieee80211_i.h
@@ -40,6 +40,22 @@ extern const struct cfg80211_ops mac80211_config_ops;

struct ieee80211_local;
struct ieee80211_mesh_fast_tx;
+/**
+ * struct ieee80211_sole_chandef - kfree_rcu-capable chandef snapshot
+ *
+ * cfg80211_chan_def has no embedded rcu_head so it cannot be freed
+ * with kfree_rcu() directly. This wrapper adds one.
+ *
+ * @rcu_head: for kfree_rcu() deferred freeing
+ * @def: point-in-time copy of the active cfg80211_chan_def
+ */
+struct ieee80211_sole_chandef {
+ struct rcu_head rcu_head;
+ struct cfg80211_chan_def def;
+};
+
+/* Defined in chan.c */
+void ieee80211_update_sole_chandef(struct ieee80211_local *local);

/* Maximum number of broadcast/multicast frames to buffer when some of the
* associated stations are using power saving. */
@@ -1586,6 +1602,7 @@ struct ieee80211_local {

/* channel contexts */
struct list_head chanctx_list;
+ struct ieee80211_sole_chandef __rcu *sole_chandef;

#ifdef CONFIG_MAC80211_LEDS
struct led_trigger tx_led, rx_led, assoc_led, radio_led;
diff --git a/net/mac80211/main.c b/net/mac80211/main.c
index 616f86b1a7e4..387ed2786b32 100644
--- a/net/mac80211/main.c
+++ b/net/mac80211/main.c
@@ -1745,6 +1745,13 @@ void ieee80211_free_hw(struct ieee80211_hw *hw)
kfree(local->hw.wiphy->bands[band]);
}

+ /*
+ * All interfaces are gone by this point, so every chanctx has been
+ * freed and ieee80211_update_sole_chandef() has already published
+ * NULL. Assert the invariant.
+ */
+ WARN_ON_ONCE(rcu_access_pointer(local->sole_chandef));
+
wiphy_free(local->hw.wiphy);
}
EXPORT_SYMBOL(ieee80211_free_hw);
diff --git a/net/mac80211/tx.c b/net/mac80211/tx.c
index b90c0537d0c5..54d06cfb670c 100644
--- a/net/mac80211/tx.c
+++ b/net/mac80211/tx.c
@@ -2398,10 +2398,73 @@ netdev_tx_t ieee80211_monitor_start_xmit(struct sk_buff *skb,
rcu_dereference(tmp_sdata->vif.bss_conf.chanctx_conf);
}

- if (chanctx_conf)
+ if (chanctx_conf) {
chandef = &chanctx_conf->def;
- else
- goto fail_rcu;
+ } else {
+ /*
+ * Real-chanctx drivers (e.g. mt76) do not assign a chanctx to
+ * the monitor VIF, so vif.bss_conf.chanctx_conf is NULL here.
+ * Fall back to the sole_chandef snapshot maintained by
+ * ieee80211_update_sole_chandef(). NULL means MCC or no active
+ * channel - drop the frame.
+ *
+ * The snapshot is valid for this whole function: it is freed
+ * via kfree_rcu() after a full grace period, and we are inside
+ * rcu_read_lock() throughout.
+ */
+ struct ieee80211_sole_chandef *sole =
+ rcu_dereference(local->sole_chandef);
+ chandef = sole ? &sole->def : NULL;
+ if (!chandef)
+ goto fail_rcu;
+ }
+
+ /*
+ * If sdata is still the monitor interface, addr2 did not match any
+ * local non-monitor interface - the normal case for injection tools
+ * (mdk4, aireplay-ng) that spoof the source MAC.
+ *
+ * On 5 GHz, cfg80211_reg_can_beacon() below commonly rejects
+ * NL80211_IFTYPE_MONITOR because a monitor interface cannot
+ * satisfy the regulatory requirements for the band (NO_IR on
+ * many channels; radar-detection responsibility on DFS channels).
+ * Pick a running non-monitor sdata operating on the same channel
+ * (identical band, width and both center frequencies) and use
+ * that for the check: an AP sdata is already authorised for the
+ * channel, so the check passes and the frame goes out on the
+ * correct channel instead of being dropped.
+ *
+ * If no such sdata exists, leave sdata as the monitor interface and
+ * let the existing code paths handle the MONITOR case (CHAN_CAN_MONITOR
+ * branch, or fail_rcu if regulatory does not permit).
+ */
+ if (sdata->vif.type == NL80211_IFTYPE_MONITOR) {
+ struct ieee80211_sub_if_data *picked = NULL;
+
+ list_for_each_entry_rcu(tmp_sdata, &local->interfaces, list) {
+ struct ieee80211_chanctx_conf *tx_conf;
+
+ if (!ieee80211_sdata_running(tmp_sdata))
+ continue;
+ if (tmp_sdata->vif.type == NL80211_IFTYPE_MONITOR ||
+ tmp_sdata->vif.type == NL80211_IFTYPE_AP_VLAN)
+ continue;
+
+ tx_conf = rcu_dereference(tmp_sdata->vif.bss_conf.chanctx_conf);
+
+ if (!tx_conf)
+ continue;
+
+ if (!cfg80211_chandef_identical(&tx_conf->def, chandef))
+ continue;
+
+ picked = tmp_sdata;
+ break;
+ }
+
+ if (picked)
+ sdata = picked;
+ }

/*
* If driver/HW supports IEEE80211_CHAN_CAN_MONITOR we still
--
2.53.0