Compare commits

..

1 Commits

Author SHA1 Message Date
claude-noether b8f57a8443 bes2600: pre-empt AP-deauth-6 with mac80211 reassoc on decrypt-fail storm
When the BES2600 firmware reports WSM_STATUS_DECRYPTFAILURE for a burst
of received frames (typically because the host's PTK or GTK has fallen
out of sync with the AP), the AP eventually concludes that the STA is
not authenticated and emits an unprotected deauth-reason-6 ("Class 2
frame received from non-authenticated station"). On the deployed
pinetab2 + bes2600 stack this AP-initiated deauth has been observed to
leave the link blackholed for up to 109 s before userspace finds a
different SSID/channel to recover on. (Receipts at
https://git.reauktion.de/marfrit/besser, notes/phase5-2026-05-06.md.)

Add a sliding-window counter on each bes2600_vif: when 5 decrypt
failures fire within 5 s, schedule a worker that calls
ieee80211_connection_loss(vif). mac80211 then performs immediate
disassociation; userspace (NetworkManager / wpa_supplicant) reconnects
with fresh keys before the AP gets a chance to fire its unprotected
deauth.

Predicted Phase 7 delta vs the unpatched baseline:
- decrypt-burst rate: unchanged (this does not address root cause)
- AP-deauth-6 rate: <= 0.2 of baseline
- conditional probability of >5s blackhole given a burst:
  100% -> <= 10%
- worst-case recovery time: 109s -> <5s

Contract pin: ieee80211_connection_loss() per
include/net/mac80211.h: "may also be called if the connection needs to
be terminated for some other reason... will cause immediate change to
disassociated state, without connection recovery attempts." Userspace
recovery is the existing NM/wpa_supplicant path. The worker context
satisfies the implicit process-context expectation.

Files touched:
- bes2600/bes2600.h: 4 new fields on struct bes2600_vif + 2 prototypes
- bes2600/txrx.c: new helpers + the call site at the existing
  WSM_STATUS_DECRYPTFAILURE log point (the unconditional "goto drop"
  branch in bes2600_rx_cb)
- bes2600/sta.c: bes2600_decrypt_storm_init() in bes2600_vif_setup
- bes2600/debug.c: DecryptStormRecoveries seq_printf in the per-vif
  status seq_file output

Threshold (5/5s) is set well above the steady-state per-vif decrypt-
fail rate observed in measurement (~1/min even under sustained 1 MB/s
load), so a true storm is required to trip it. The cw1200/cw1260
ancestor has no equivalent storm-recovery; this is a clean addition.

checkpatch.pl --no-tree --strict: clean (0/0/0).

Signed-off-by: Claude (noether) <claude@reauktion.de>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 19:50:52 +02:00
5 changed files with 89 additions and 17 deletions
+9
View File
@@ -596,6 +596,11 @@ struct bes2600_vif {
unsigned long rx_timestamp; unsigned long rx_timestamp;
u32 cipherType; u32 cipherType;
/* Decrypt-storm fast-recover (Trigger B). See txrx.c. */
unsigned long decrypt_storm_window_start;
unsigned int decrypt_storm_count;
unsigned int decrypt_storm_recoveries;
struct work_struct decrypt_storm_recover_work;
/* AP powersave */ /* AP powersave */
u32 link_id_map; u32 link_id_map;
@@ -856,4 +861,8 @@ int bes2600_btusb_setup_pipes(struct sbus_priv *sbus_priv);
void bes2600_btusb_uninit(struct usb_interface *interface); void bes2600_btusb_uninit(struct usb_interface *interface);
#endif #endif
/* Decrypt-storm fast-recover helpers — see txrx.c. */
void bes2600_decrypt_storm_init(struct bes2600_vif *priv);
void bes2600_decrypt_storm_account(struct bes2600_vif *priv);
#endif /* BES2600_H */ #endif /* BES2600_H */
+3 -17
View File
@@ -472,7 +472,6 @@ static int bes2600_pwr_enter_lp_mode(struct bes2600_common *hw_priv)
int i = 0; int i = 0;
struct bes2600_vif *priv; struct bes2600_vif *priv;
int ret = 0; int ret = 0;
int timeouts = 0;
char ip_str[20]; char ip_str[20];
unsigned long status = 0; unsigned long status = 0;
@@ -529,35 +528,22 @@ static int bes2600_pwr_enter_lp_mode(struct bes2600_common *hw_priv)
if (ret) { if (ret) {
atomic_set(&hw_priv->bes_power.pm_set_in_process, 0); atomic_set(&hw_priv->bes_power.pm_set_in_process, 0);
bes_err("%s, set operation mode fail\n", __func__); bes_err("%s, set operation mode fail\n", __func__);
timeouts++;
continue;
} }
/* wait power save mode changed indication */ /* wait power save mode changed indication */
status = wait_for_completion_timeout(&hw_priv->bes_power.pm_enter_cmpl, 5 * HZ); status = wait_for_completion_timeout(&hw_priv->bes_power.pm_enter_cmpl, 5 * HZ);
atomic_set(&hw_priv->bes_power.pm_set_in_process, 0); atomic_set(&hw_priv->bes_power.pm_set_in_process, 0);
reinit_completion(&hw_priv->bes_power.pm_enter_cmpl); reinit_completion(&hw_priv->bes_power.pm_enter_cmpl);
if (!status) { if (!status)
bes_err("%s, wait pm ind timeout\n", __func__); bes_err("%s, wait pm ind timeout\n", __func__);
timeouts++;
}
} else { } else {
bes_devel("skip enter lp mode\n"); bes_devel("skip enter lp mode\n");
} }
} }
} }
/* /* set device low power configuration */
* Enter the device-end of the LP transition only if every per-VIF bes2600_pwr_device_enter_lp_mode(hw_priv);
* mac80211 handshake reached firmware-ACKed completion. Doing the
* device-LP setup while any VIF is still pending leaves the driver
* in an inconsistent state that cascades into SDIO TX errors on
* the BES2600.
*/
if (timeouts == 0)
bes2600_pwr_device_enter_lp_mode(hw_priv);
else
ret = -ETIMEDOUT;
return ret; return ret;
} }
+2
View File
@@ -542,6 +542,8 @@ static int bes2600_status_show_priv(struct seq_file *seq, void *v)
priv->listening ? " (listening)" : ""); priv->listening ? " (listening)" : "");
seq_printf(seq, "Assoc: %s\n", seq_printf(seq, "Assoc: %s\n",
bes2600_debug_join_status[priv->join_status]); bes2600_debug_join_status[priv->join_status]);
seq_printf(seq, "DecryptStormRecoveries: %u\n",
priv->decrypt_storm_recoveries);
if (priv->rx_filter.promiscuous) if (priv->rx_filter.promiscuous)
seq_puts(seq, "Filter: promisc\n"); seq_puts(seq, "Filter: promisc\n");
else if (priv->rx_filter.fcs) else if (priv->rx_filter.fcs)
+1
View File
@@ -2619,6 +2619,7 @@ int bes2600_vif_setup(struct bes2600_vif *priv)
/* Setup per vif workitems and locks */ /* Setup per vif workitems and locks */
spin_lock_init(&priv->vif_lock); spin_lock_init(&priv->vif_lock);
bes2600_decrypt_storm_init(priv);
INIT_WORK(&priv->join_work, bes2600_join_work); INIT_WORK(&priv->join_work, bes2600_join_work);
INIT_DELAYED_WORK(&priv->join_timeout, bes2600_join_timeout); INIT_DELAYED_WORK(&priv->join_timeout, bes2600_join_timeout);
INIT_WORK(&priv->unjoin_work, bes2600_unjoin_work); INIT_WORK(&priv->unjoin_work, bes2600_unjoin_work);
+74
View File
@@ -25,6 +25,78 @@
#define BES2600_INVALID_RATE_ID (0xFF) #define BES2600_INVALID_RATE_ID (0xFF)
/*
* Decrypt-storm fast-recover (Trigger B).
*
* When the BES2600 firmware reports WSM_STATUS_DECRYPTFAILURE for a
* burst of received frames (typically because the host's PTK or GTK
* has fallen out of sync with the AP), the AP eventually concludes that
* the STA is not authenticated and emits an unprotected deauth-reason-6
* ("Class 2 frame received from non-authenticated station"). On the
* deployed pinetab2 + bes2600 stack this AP-initiated deauth has been
* observed to leave the link blackholed for up to 109 s before
* userspace finds a different SSID/channel to recover on. (Receipts at
* https://git.reauktion.de/marfrit/besser, notes/phase5-2026-05-06.md.)
*
* Recovery here pre-empts the AP: when we see THRESHOLD decrypt
* failures within WINDOW, we ask mac80211 for a clean reassoc via
* ieee80211_connection_loss(), which causes immediate disassociation
* and lets userspace auto-reconnect with fresh keys.
*
* mac80211 contract: ieee80211_connection_loss() may be called
* regardless of IEEE80211_HW_CONNECTION_MONITOR; it causes immediate
* disassociation without driver-side recovery attempts. See
* include/net/mac80211.h for the canonical doc-comment.
*
* The threshold is set well above the steady-state per-vif
* decrypt-fail rate observed in measurement (~1/min even under
* sustained 1 MB/s load), so a true storm is required to trip it.
*/
#define BES2600_DECRYPT_STORM_THRESHOLD 5
#define BES2600_DECRYPT_STORM_WINDOW_MS 5000
static void bes2600_decrypt_storm_recover_work(struct work_struct *work)
{
struct bes2600_vif *priv = container_of(work, struct bes2600_vif,
decrypt_storm_recover_work);
if (!priv->vif)
return;
bes_warn("[bes2600] decrypt-storm fast-recover: forcing reassoc\n");
ieee80211_connection_loss(priv->vif);
priv->decrypt_storm_recoveries++;
}
void bes2600_decrypt_storm_init(struct bes2600_vif *priv)
{
INIT_WORK(&priv->decrypt_storm_recover_work,
bes2600_decrypt_storm_recover_work);
priv->decrypt_storm_window_start = 0;
priv->decrypt_storm_count = 0;
priv->decrypt_storm_recoveries = 0;
}
void bes2600_decrypt_storm_account(struct bes2600_vif *priv)
{
unsigned long now = jiffies;
unsigned long window = msecs_to_jiffies(BES2600_DECRYPT_STORM_WINDOW_MS);
if (priv->decrypt_storm_window_start == 0 ||
time_after(now, priv->decrypt_storm_window_start + window)) {
priv->decrypt_storm_window_start = now;
priv->decrypt_storm_count = 1;
return;
}
if (++priv->decrypt_storm_count >= BES2600_DECRYPT_STORM_THRESHOLD) {
priv->decrypt_storm_count = 0;
/* Skew the window so we don't re-fire on the same storm. */
priv->decrypt_storm_window_start = now + window;
schedule_work(&priv->decrypt_storm_recover_work);
}
}
#ifdef CONFIG_BES2600_TESTMODE #ifdef CONFIG_BES2600_TESTMODE
#include "bes_nl80211_testmode_msg.h" #include "bes_nl80211_testmode_msg.h"
#endif /* CONFIG_BES2600_TESTMODE */ #endif /* CONFIG_BES2600_TESTMODE */
@@ -1672,6 +1744,8 @@ void bes2600_rx_cb(struct bes2600_vif *priv,
goto drop; goto drop;
} else { } else {
bes_warn("[RX] Receive failure: %d.\n", arg->status); bes_warn("[RX] Receive failure: %d.\n", arg->status);
if (arg->status == WSM_STATUS_DECRYPTFAILURE)
bes2600_decrypt_storm_account(priv);
goto drop; goto drop;
} }
} }