From af4b52affcffa2ceee2b49ed08b85db9a73edeb1 Mon Sep 17 00:00:00 2001 From: "Claude (noether)" Date: Wed, 6 May 2026 19:50:52 +0200 Subject: [PATCH] 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; cancel_work_sync() in bes2600_remove_interface, alongside the existing per-vif cancel_*_work_sync block. Safe under the kernel cancel_work_sync contract: the work_struct is INIT_WORK'd in setup, so the call is valid; it blocks until any in-flight handler returns, ensuring no use-after-free of priv when mac80211 frees the vif; and it is idempotent (subsequent calls just return false). - 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) Co-Authored-By: Claude Opus 4.7 (1M context) --- bes2600/bes2600.h | 9 ++++++ bes2600/debug.c | 2 ++ bes2600/sta.c | 2 ++ bes2600/txrx.c | 74 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 87 insertions(+) diff --git a/bes2600/bes2600.h b/bes2600/bes2600.h index 0e60960..66482f7 100644 --- a/bes2600/bes2600.h +++ b/bes2600/bes2600.h @@ -596,6 +596,11 @@ struct bes2600_vif { unsigned long rx_timestamp; 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 */ 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); #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 */ diff --git a/bes2600/debug.c b/bes2600/debug.c index 5228b22..ca223dd 100644 --- a/bes2600/debug.c +++ b/bes2600/debug.c @@ -542,6 +542,8 @@ static int bes2600_status_show_priv(struct seq_file *seq, void *v) priv->listening ? " (listening)" : ""); seq_printf(seq, "Assoc: %s\n", bes2600_debug_join_status[priv->join_status]); + seq_printf(seq, "DecryptStormRecoveries: %u\n", + priv->decrypt_storm_recoveries); if (priv->rx_filter.promiscuous) seq_puts(seq, "Filter: promisc\n"); else if (priv->rx_filter.fcs) diff --git a/bes2600/sta.c b/bes2600/sta.c index aa69eb8..0723a7c 100644 --- a/bes2600/sta.c +++ b/bes2600/sta.c @@ -448,6 +448,7 @@ void bes2600_remove_interface(struct ieee80211_hw *dev, cancel_delayed_work_sync(&priv->join_timeout); cancel_delayed_work_sync(&priv->set_cts_work); cancel_delayed_work_sync(&priv->pending_offchanneltx_work); + cancel_work_sync(&priv->decrypt_storm_recover_work); del_timer_sync(&priv->mcast_timeout); /* TODO:COMBO: May be reset of these variables "delayed_link_loss and @@ -2619,6 +2620,7 @@ int bes2600_vif_setup(struct bes2600_vif *priv) /* Setup per vif workitems and locks */ spin_lock_init(&priv->vif_lock); + bes2600_decrypt_storm_init(priv); INIT_WORK(&priv->join_work, bes2600_join_work); INIT_DELAYED_WORK(&priv->join_timeout, bes2600_join_timeout); INIT_WORK(&priv->unjoin_work, bes2600_unjoin_work); diff --git a/bes2600/txrx.c b/bes2600/txrx.c index dbd1b23..346312c 100644 --- a/bes2600/txrx.c +++ b/bes2600/txrx.c @@ -25,6 +25,78 @@ #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 #include "bes_nl80211_testmode_msg.h" #endif /* CONFIG_BES2600_TESTMODE */ @@ -1672,6 +1744,8 @@ void bes2600_rx_cb(struct bes2600_vif *priv, goto drop; } else { bes_warn("[RX] Receive failure: %d.\n", arg->status); + if (arg->status == WSM_STATUS_DECRYPTFAILURE) + bes2600_decrypt_storm_account(priv); goto drop; } }