wifi: mac80211: add a workaround for receiving non-standard mesh A-MSDU
authorFelix Fietkau <nbd@nbd.name>
Mon, 13 Feb 2023 10:08:55 +0000 (11:08 +0100)
committerJohannes Berg <johannes.berg@intel.com>
Tue, 14 Feb 2023 11:35:02 +0000 (12:35 +0100)
At least ath10k and ath11k supported hardware (maybe more) does not implement
mesh A-MSDU aggregation in a standard compliant way.
802.11-2020 9.3.2.2.2 declares that the Mesh Control field is part of the
A-MSDU header (and little-endian).
As such, its length must not be included in the subframe length field.
Hardware affected by this bug treats the mesh control field as part of the
MSDU data and sets the length accordingly.
In order to avoid packet loss, keep track of which stations are affected
by this and take it into account when converting A-MSDU to 802.3 + mesh control
packets.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
Link: https://lore.kernel.org/r/20230213100855.34315-5-nbd@nbd.name
Signed-off-by: Johannes Berg <johannes.berg@intel.com>
include/net/cfg80211.h
net/mac80211/rx.c
net/mac80211/sta_info.c
net/mac80211/sta_info.h
net/wireless/util.c

index c506dc1286852cdb0822cf73bf300c5060cb1c56..9b015bb877db2cc75f2f2879229bc2aa4fb90ab2 100644 (file)
@@ -6236,6 +6236,19 @@ static inline int ieee80211_data_to_8023(struct sk_buff *skb, const u8 *addr,
        return ieee80211_data_to_8023_exthdr(skb, NULL, addr, iftype, 0, false);
 }
 
+/**
+ * ieee80211_is_valid_amsdu - check if subframe lengths of an A-MSDU are valid
+ *
+ * This is used to detect non-standard A-MSDU frames, e.g. the ones generated
+ * by ath10k and ath11k, where the subframe length includes the length of the
+ * mesh control field.
+ *
+ * @skb: The input A-MSDU frame without any headers.
+ * @mesh_hdr: use standard compliant mesh A-MSDU subframe header
+ * Returns: true if subframe header lengths are valid for the @mesh_hdr mode
+ */
+bool ieee80211_is_valid_amsdu(struct sk_buff *skb, bool mesh_hdr);
+
 /**
  * ieee80211_amsdu_to_8023s - decode an IEEE 802.11n A-MSDU frame
  *
index b5ee049e31977c47803c52e96391b4fca092c97f..759936aff2c3ca878144c163753d9b4ff0b82423 100644 (file)
@@ -2899,7 +2899,6 @@ __ieee80211_rx_h_amsdu(struct ieee80211_rx_data *rx, u8 data_offset)
        static ieee80211_rx_result res;
        struct ethhdr ethhdr;
        const u8 *check_da = ethhdr.h_dest, *check_sa = ethhdr.h_source;
-       bool mesh = false;
 
        if (unlikely(ieee80211_has_a4(hdr->frame_control))) {
                check_da = NULL;
@@ -2917,7 +2916,6 @@ __ieee80211_rx_h_amsdu(struct ieee80211_rx_data *rx, u8 data_offset)
                case NL80211_IFTYPE_MESH_POINT:
                        check_sa = NULL;
                        check_da = NULL;
-                       mesh = true;
                        break;
                default:
                        break;
@@ -2932,10 +2930,21 @@ __ieee80211_rx_h_amsdu(struct ieee80211_rx_data *rx, u8 data_offset)
                                          data_offset, true))
                return RX_DROP_UNUSABLE;
 
+       if (rx->sta && rx->sta->amsdu_mesh_control < 0) {
+               bool valid_std = ieee80211_is_valid_amsdu(skb, true);
+               bool valid_nonstd = ieee80211_is_valid_amsdu(skb, false);
+
+               if (valid_std && !valid_nonstd)
+                       rx->sta->amsdu_mesh_control = 1;
+               else if (valid_nonstd && !valid_std)
+                       rx->sta->amsdu_mesh_control = 0;
+       }
+
        ieee80211_amsdu_to_8023s(skb, &frame_list, dev->dev_addr,
                                 rx->sdata->vif.type,
                                 rx->local->hw.extra_tx_headroom,
-                                check_da, check_sa, mesh);
+                                check_da, check_sa,
+                                rx->sta->amsdu_mesh_control);
 
        while (!skb_queue_empty(&frame_list)) {
                rx->skb = __skb_dequeue(&frame_list);
index bd532d3f925dde78c13020532f1a206bef9b82ec..7d68dbc872d7736f5d72eeb1b6f64ddbdf67ca6f 100644 (file)
@@ -594,6 +594,9 @@ __sta_info_alloc(struct ieee80211_sub_if_data *sdata,
 
        sta->sta_state = IEEE80211_STA_NONE;
 
+       if (sdata->vif.type == NL80211_IFTYPE_MESH_POINT)
+               sta->amsdu_mesh_control = -1;
+
        /* Mark TID as unreserved */
        sta->reserved_tid = IEEE80211_TID_UNRESERVED;
 
index c30f02874fb197dceadaf93c846a12ae783c7b9e..5a1c541bb4ac7bd8c12edcd0c4025d798510fd05 100644 (file)
@@ -707,6 +707,7 @@ struct sta_info {
        struct codel_params cparams;
 
        u8 reserved_tid;
+       s8 amsdu_mesh_control;
 
        struct cfg80211_chan_def tdls_chandef;
 
index 8ae117cfc22cfff88b4df1f765494cf0e99abb2b..d1a89e82ead08d1d9837c4ea4bde8bb8bcc2d5c6 100644 (file)
@@ -776,6 +776,38 @@ __ieee80211_amsdu_copy(struct sk_buff *skb, unsigned int hlen,
        return frame;
 }
 
+bool ieee80211_is_valid_amsdu(struct sk_buff *skb, bool mesh_hdr)
+{
+       int offset = 0, remaining, subframe_len, padding;
+
+       for (offset = 0; offset < skb->len; offset += subframe_len + padding) {
+               struct {
+                   __be16 len;
+                   u8 mesh_flags;
+               } hdr;
+               u16 len;
+
+               if (skb_copy_bits(skb, offset + 2 * ETH_ALEN, &hdr, sizeof(hdr)) < 0)
+                       return false;
+
+               if (mesh_hdr)
+                       len = le16_to_cpu(*(__le16 *)&hdr.len) +
+                             __ieee80211_get_mesh_hdrlen(hdr.mesh_flags);
+               else
+                       len = ntohs(hdr.len);
+
+               subframe_len = sizeof(struct ethhdr) + len;
+               padding = (4 - subframe_len) & 0x3;
+               remaining = skb->len - offset;
+
+               if (subframe_len > remaining)
+                       return false;
+       }
+
+       return true;
+}
+EXPORT_SYMBOL(ieee80211_is_valid_amsdu);
+
 void ieee80211_amsdu_to_8023s(struct sk_buff *skb, struct sk_buff_head *list,
                              const u8 *addr, enum nl80211_iftype iftype,
                              const unsigned int extra_headroom,