| Seq | Timestamp | Type | Name | Duration |
|---|---|---|---|---|
| 0/0 | 2026/09/24 13:41 | flow | patch-triage |
4mArgs:null Results: {
"EnableConfigs": [
"CFG80211",
"MAC80211",
"MAC80211_HWSIM",
"CFG80211_DEBUGFS",
"MAC80211_DEBUGFS",
"DEBUG_FS"
],
"FocusSymbols": [
"nl80211_set_scs",
"nl80211_set_mscs",
"nl80211_parse_scs_desc",
"ieee80211_set_scs",
"ieee80211_set_mscs",
"ieee80211_flow_classify",
"ieee80211_flow_learn",
"cfg80211_parse_tclas",
"cfg80211_parse_tclas_mask",
"cfg80211_flow_parse",
"cfg80211_flow_key_build",
"cfg80211_scs_evaluate",
"mac80211_hwsim_sta_set_scs"
],
"KMSANReasoning": "The patch series implements Stream Classification Service (SCS) and Mirrored SCS (MSCS) support across cfg80211, mac80211, and the mt7996 / mac80211_hwsim drivers.\n\nKMSAN vs KASAN Applicability Analysis:\n1. User-space copies / Info-leaks: Responses to netlink commands (NL80211_CMD_SET_SCS and NL80211_CMD_SET_MSCS) send discrete scalar values using nla_put_u8 and nla_put_u16 rather than writing entire structures or aggregates containing padding bytes. There are no kernel-to-user info-leak risks.\n2. Structure allocations and initializations: All newly introduced heap allocations (such as cfg80211_scs_desc, ieee80211_scs_sta, ieee80211_mscs_sta, and ieee80211_flow_entry) are allocated with kzalloc, kvzalloc, kcalloc, or kzalloc_obj. Stack structures (e.g., cfg80211_flow_info, cfg80211_flow_key, cfg80211_tclas, cfg80211_mscs_desc) are consistently zeroed either at declaration via '= {}' or through explicit memset() calls before parsing and manipulation.\n3. Flow key comparison and hashing: Structures compared with memcmp or hashed via rhashtable (cfg80211_flow_key and ieee80211_flow_hkey) have their layouts verified with static_assert against padding, and rhashtable key lengths strictly bound the lookup key length to offsetofend(..., key).\n4. Firmware communication: In mt7996_mcu_set_scs, the firmware request struct is initialized with designated initializers '= { ... }', ensuring all padding and reserved fields are zero-filled.\n5. Bug profile: The primary risk surface of the added element parsers, frame classifiers, and driver callbacks involves out-of-bounds array reads, parser boundary validation, concurrency/locking, or object lifetime management. These failure modes are comprehensively covered by KASAN, UBSAN, and LOCKDEP.\n\nBecause no uninitialized memory usage or info-leak vectors are introduced, a dedicated KMSAN fuzzing session is not justified.",
"NeedsKMSAN": false,
"Reasoning": "The patch series implements support for Stream Classification Service (SCS) and Mirrored SCS (MSCS) across cfg80211, mac80211, and the virtual mac80211_hwsim driver. It introduces new user-accessible netlink commands (NL80211_CMD_SET_SCS and NL80211_CMD_SET_MSCS), TCLAS element parsing, flow key matching, rhashtable management, and packet classification in RX/TX fast paths. All of this code is directly reachable in standard virtualized environments (e.g. QEMU) using mac80211_hwsim and nl80211 netlink sockets, making it highly valuable for fuzzing.",
"WorthFuzzing": true
} |
| 1/1 | 2026/09/24 13:41 | action | read-patch-diff |
0mArgs:null Results: {
"PatchDiff": "commit 80a1526dde5ea12c2da885d6b6b0e2c4c75ac2a4\nAuthor: syz-cluster \u003ctriage@syzkaller.com\u003e\nDate: Thu Sep 24 13:41:14 2026 +0000\n\n syz-cluster: applied patch under review\n\ndiff --git a/drivers/net/wireless/mediatek/mt76/mt7996/init.c b/drivers/net/wireless/mediatek/mt76/mt7996/init.c\nindex fb635a0925840..ca161d644d728 100644\n--- a/drivers/net/wireless/mediatek/mt76/mt7996/init.c\n+++ b/drivers/net/wireless/mediatek/mt76/mt7996/init.c\n@@ -526,6 +526,8 @@ mt7996_init_wiphy(struct ieee80211_hw *hw, struct mtk_wed_device *wed)\n \twiphy_ext_feature_set(wiphy, NL80211_EXT_FEATURE_CAN_REPLACE_PTK0);\n \twiphy_ext_feature_set(wiphy, NL80211_EXT_FEATURE_MU_MIMO_AIR_SNIFFER);\n \twiphy_ext_feature_set(wiphy, NL80211_EXT_FEATURE_SET_SCAN_DWELL);\n+\twiphy_ext_feature_set(wiphy, NL80211_EXT_FEATURE_SCS);\n+\twiphy_ext_feature_set(wiphy, NL80211_EXT_FEATURE_MSCS);\n \n \tif (mt7996_eeprom_has_background_radar(dev) \u0026\u0026\n \t (!mdev-\u003edev-\u003eof_node ||\n@@ -1580,6 +1582,7 @@ mt7996_init_eht_caps(struct mt7996_phy *phy, enum nl80211_band band,\n \n \teht_cap_elem-\u003emac_cap_info[0] =\n \t\tIEEE80211_EHT_MAC_CAP0_OM_CONTROL |\n+\t\tIEEE80211_EHT_MAC_CAP0_SCS_TRAFFIC_DESC |\n \t\tu8_encode_bits(IEEE80211_EHT_MAC_CAP0_MAX_MPDU_LEN_11454,\n \t\t\t IEEE80211_EHT_MAC_CAP0_MAX_MPDU_LEN_MASK);\n \ndiff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c\nindex e218856b0c454..6b2d24fe70f10 100644\n--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c\n+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c\n@@ -2051,6 +2051,69 @@ static void mt7996_sta_set_4addr(struct ieee80211_hw *hw,\n \tmutex_unlock(\u0026dev-\u003emt76.mutex);\n }\n \n+/* SCS descriptors belong to the MLD, firmware streams to a link */\n+static int mt7996_sta_scs_program(struct mt7996_dev *dev,\n+\t\t\t\t struct ieee80211_vif *vif,\n+\t\t\t\t struct ieee80211_sta *sta,\n+\t\t\t\t const struct cfg80211_scs_desc *desc,\n+\t\t\t\t bool del)\n+{\n+\tstruct mt7996_sta *msta = (struct mt7996_sta *)sta-\u003edrv_priv;\n+\tstruct ieee80211_link_sta *link_sta;\n+\tunsigned int link_id;\n+\tbool any = false;\n+\tint ret = 0;\n+\n+\tfor_each_sta_active_link(vif, sta, link_sta, link_id) {\n+\t\tstruct mt7996_sta_link *msta_link;\n+\n+\t\tmsta_link = mt7996_sta_link_protected(dev, msta, link_id);\n+\t\tif (!msta_link || !msta_link-\u003ewcid.sta)\n+\t\t\tcontinue;\n+\n+\t\tany = true;\n+\t\tif (mt7996_mcu_set_scs(dev, msta_link-\u003ewcid.idx, desc, del))\n+\t\t\tret = -EIO;\n+\t}\n+\n+\treturn any ? ret : -EIO;\n+}\n+\n+static int mt7996_sta_set_scs(struct ieee80211_hw *hw,\n+\t\t\t struct ieee80211_vif *vif,\n+\t\t\t struct ieee80211_sta *sta,\n+\t\t\t struct cfg80211_scs_desc * const *desc,\n+\t\t\t struct cfg80211_scs_result *res, u8 n_desc)\n+{\n+\tstruct mt7996_dev *dev = mt7996_hw_dev(hw);\n+\tu8 i;\n+\n+\tmutex_lock(\u0026dev-\u003emt76.mutex);\n+\n+\tfor (i = 0; i \u003c n_desc; i++) {\n+\t\tconst struct cfg80211_scs_desc *d = desc[i];\n+\t\tbool del = d-\u003ereq_type == NL80211_SCS_REQ_REMOVE ||\n+\t\t\t !d-\u003eqos_char;\n+\n+\t\t/* The firmware only supports uplink streams */\n+\t\tif (!del \u0026\u0026 ieee80211_qos_char_direction(d-\u003eqos_char) !=\n+\t\t\t IEEE80211_QOS_CHAR_DIR_UPLINK) {\n+\t\t\tres[i].status = WLAN_STATUS_REQUEST_DECLINED;\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\tif (!mt7996_sta_scs_program(dev, vif, sta, d, del) || del)\n+\t\t\tcontinue;\n+\n+\t\tmt7996_sta_scs_program(dev, vif, sta, d, true);\n+\t\tres[i].status = WLAN_STATUS_REQUEST_DECLINED;\n+\t}\n+\n+\tmutex_unlock(\u0026dev-\u003emt76.mutex);\n+\n+\treturn 0;\n+}\n+\n static void mt7996_sta_set_decap_offload(struct ieee80211_hw *hw,\n \t\t\t\t\t struct ieee80211_vif *vif,\n \t\t\t\t\t struct ieee80211_sta *sta,\n@@ -2588,6 +2651,7 @@ const struct ieee80211_ops mt7996_ops = {\n \t.set_coverage_class = mt7996_set_coverage_class,\n \t.sta_statistics = mt7996_sta_statistics,\n \t.sta_set_4addr = mt7996_sta_set_4addr,\n+\t.sta_set_scs = mt7996_sta_set_scs,\n \t.sta_set_decap_offload = mt7996_sta_set_decap_offload,\n \t.add_twt_setup = mt7996_mac_add_twt_setup,\n \t.twt_teardown_request = mt7996_twt_teardown_request,\ndiff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c\nindex cd7587ce5b8a6..fceffdeb8ca50 100644\n--- a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c\n+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c\n@@ -2616,6 +2616,45 @@ int mt7996_mcu_add_rate_ctrl(struct mt7996_dev *dev, struct mt7996_sta *msta,\n \treturn ret;\n }\n \n+int mt7996_mcu_set_scs(struct mt7996_dev *dev, u16 wlan_idx,\n+\t\t const struct cfg80211_scs_desc *desc, bool del)\n+{\n+#define MURU_QOS_FLAG_UPDATE\t20\n+#define MURU_QOS_FLAG_DELETE\t21\n+\tstruct {\n+\t\tu8 __rsv1[4];\n+\n+\t\t__le16 tag;\n+\t\t__le16 len;\n+\t\t__le32 qos_flag;\n+\t\t__le16 wlan_idx;\n+\t\tu8 __rsv2[12];\n+\t\tu8 dir;\n+\t\tu8 __rsv3[4];\n+\t\tu8 scs_id;\n+\t\tu8 qos_ie[44];\n+\t} __packed req = {\n+\t\t.tag = cpu_to_le16(UNI_CMD_MURU_SET_QOS_CFG),\n+\t\t.len = cpu_to_le16(sizeof(req) - 4),\n+\t\t.wlan_idx = cpu_to_le16(wlan_idx),\n+\t\t.scs_id = desc-\u003eid,\n+\t};\n+\n+\tif (del) {\n+\t\treq.qos_flag = cpu_to_le32(MURU_QOS_FLAG_DELETE);\n+\t} else {\n+\t\tif (desc-\u003eqos_char_len \u003e sizeof(req.qos_ie))\n+\t\t\treturn -EINVAL;\n+\n+\t\treq.qos_flag = cpu_to_le32(MURU_QOS_FLAG_UPDATE);\n+\t\treq.dir = ieee80211_qos_char_direction(desc-\u003eqos_char);\n+\t\tmemcpy(req.qos_ie, desc-\u003eqos_char, desc-\u003eqos_char_len);\n+\t}\n+\n+\treturn mt76_mcu_send_msg(\u0026dev-\u003emt76, MCU_WMWA_UNI_CMD(MURU), \u0026req,\n+\t\t\t\t sizeof(req), true);\n+}\n+\n static int\n mt7996_mcu_add_group(struct mt7996_dev *dev, struct mt7996_vif_link *link,\n \t\t struct mt76_wcid *wcid)\ndiff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.h b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.h\nindex 74b70fb6da3da..efd474cf8da9c 100644\n--- a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.h\n+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.h\n@@ -990,6 +990,10 @@ enum {\n \tUNI_EXT_EEPROM_ACCESS = 1,\n };\n \n+enum {\n+\tUNI_CMD_MURU_SET_QOS_CFG = 0xfe,\n+};\n+\n enum {\n \tUNI_VOW_DRR_CTRL,\n \tUNI_VOW_RX_AT_AIRTIME_EN = 0x0b,\ndiff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h b/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h\nindex 2a0cdb56f822e..ebd55d8da654f 100644\n--- a/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h\n+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h\n@@ -754,6 +754,8 @@ int mt7996_mcu_set_fixed_rate_ctrl(struct mt7996_dev *dev,\n \t\t\t\t void *data, u16 version);\n int mt7996_mcu_set_fixed_field(struct mt7996_dev *dev, struct mt7996_sta *msta,\n \t\t\t void *data, u8 link_id, u32 field);\n+int mt7996_mcu_set_scs(struct mt7996_dev *dev, u16 wlan_idx,\n+\t\t const struct cfg80211_scs_desc *desc, bool del);\n int mt7996_mcu_set_eeprom(struct mt7996_dev *dev);\n int mt7996_mcu_get_eeprom(struct mt7996_dev *dev, u32 offset, u8 *buf, u32 buf_len,\n \t\t\t enum mt7996_eeprom_mode mode);\ndiff --git a/drivers/net/wireless/virtual/mac80211_hwsim_i.h b/drivers/net/wireless/virtual/mac80211_hwsim_i.h\nindex 0f0f2ac6d80eb..7f1c85ea6813e 100644\n--- a/drivers/net/wireless/virtual/mac80211_hwsim_i.h\n+++ b/drivers/net/wireless/virtual/mac80211_hwsim_i.h\n@@ -137,6 +137,9 @@ struct mac80211_hwsim_data {\n \t/* RSSI in rx status of the receiver */\n \tint rx_rssi;\n \n+\t/* Status code for a traffic description, 0 accepts */\n+\tu16 scs_status;\n+\n \t/* only used when pmsr capability is supplied */\n \tstruct cfg80211_pmsr_capabilities pmsr_capa;\n \tstruct cfg80211_pmsr_request *pmsr_request;\ndiff --git a/drivers/net/wireless/virtual/mac80211_hwsim_main.c b/drivers/net/wireless/virtual/mac80211_hwsim_main.c\nindex d567ed8d45a43..828dde53747a5 100644\n--- a/drivers/net/wireless/virtual/mac80211_hwsim_main.c\n+++ b/drivers/net/wireless/virtual/mac80211_hwsim_main.c\n@@ -4294,6 +4294,24 @@ static int mac80211_hwsim_set_radar_background(struct ieee80211_hw *hw,\n \treturn 0;\n }\n \n+/* The status code for a traffic description is set via debugfs for testing */\n+static int mac80211_hwsim_sta_set_scs(struct ieee80211_hw *hw,\n+\t\t\t\t struct ieee80211_vif *vif,\n+\t\t\t\t struct ieee80211_sta *sta,\n+\t\t\t\t struct cfg80211_scs_desc * const *desc,\n+\t\t\t\t struct cfg80211_scs_result *res,\n+\t\t\t\t u8 n_desc)\n+{\n+\tstruct mac80211_hwsim_data *data = hw-\u003epriv;\n+\tu8 i;\n+\n+\tfor (i = 0; i \u003c n_desc; i++)\n+\t\tif (desc[i]-\u003eqos_char)\n+\t\t\tres[i].status = data-\u003escs_status;\n+\n+\treturn 0;\n+}\n+\n #ifdef CONFIG_MAC80211_DEBUGFS\n #define HWSIM_DEBUGFS_OPS\t\t\t\t\t\\\n \t.link_add_debugfs = mac80211_hwsim_link_add_debugfs,\n@@ -4318,6 +4336,7 @@ static int mac80211_hwsim_set_radar_background(struct ieee80211_hw *hw,\n \t.link_sta_rc_update = mac80211_hwsim_sta_rc_update,\t\\\n \t.conf_tx = mac80211_hwsim_conf_tx,\t\t\t\\\n \t.get_survey = mac80211_hwsim_get_survey,\t\t\\\n+\t.sta_set_scs = mac80211_hwsim_sta_set_scs,\t\t\\\n \tCFG80211_TESTMODE_CMD(mac80211_hwsim_testmode_cmd)\t\\\n \t.ampdu_action = mac80211_hwsim_ampdu_action,\t\t\\\n \t.flush = mac80211_hwsim_flush,\t\t\t\t\\\n@@ -5973,6 +5992,8 @@ static int mac80211_hwsim_new_radio(struct genl_info *info,\n \t\t\t NL80211_EXT_FEATURE_EXT_KEY_ID);\n \twiphy_ext_feature_set(hw-\u003ewiphy,\n \t\t\t NL80211_EXT_FEATURE_ASSOC_FRAME_ENCRYPTION);\n+\twiphy_ext_feature_set(hw-\u003ewiphy, NL80211_EXT_FEATURE_SCS);\n+\twiphy_ext_feature_set(hw-\u003ewiphy, NL80211_EXT_FEATURE_MSCS);\n \n \thw-\u003ewiphy-\u003einterface_modes = param-\u003eiftypes;\n \n@@ -6162,6 +6183,8 @@ static int mac80211_hwsim_new_radio(struct genl_info *info,\n \t\t\t \u0026hwsim_fops_group);\n \tdebugfs_create_file(\"rx_rssi\", 0666, data-\u003edebugfs, data,\n \t\t\t \u0026hwsim_fops_rx_rssi);\n+\tdebugfs_create_u16(\"scs_status\", 0600, data-\u003edebugfs,\n+\t\t\t \u0026data-\u003escs_status);\n \tif (!data-\u003euse_chanctx)\n \t\tdebugfs_create_file(\"dfs_simulate_radar\", 0222,\n \t\t\t\t data-\u003edebugfs,\ndiff --git a/include/linux/ieee80211-eht.h b/include/linux/ieee80211-eht.h\nindex a699d108eee61..92fa7efdf156c 100644\n--- a/include/linux/ieee80211-eht.h\n+++ b/include/linux/ieee80211-eht.h\n@@ -1385,4 +1385,129 @@ static inline u32 ieee80211_eml_trans_timeout_in_us(u16 eml_cap)\n \t\t\t\t _data + ieee80211_mle_common_size(_data),\\\n \t\t\t\t _len - ieee80211_mle_common_size(_data))\n \n+/**\n+ * struct ieee80211_qos_char_elem - QoS Characteristics element\n+ * @element_id: %WLAN_EID_EXTENSION\n+ * @length: length of everything behind it\n+ * @element_id_ext: %WLAN_EID_EXT_QOS_CHARACTERISTICS\n+ * @control: control info, see %IEEE80211_QOS_CHAR_CTRL_*\n+ * @min_service_interval: minimum service period interval, in microseconds\n+ * @max_service_interval: maximum service period interval, in microseconds\n+ * @min_data_rate: minimum data rate at the MAC SAP, in kilobits per second\n+ * @delay_bound: targeted transport time of an MSDU, in microseconds\n+ * @variable: the optional parameters that the presence bitmap names, in the\n+ *\torder of the %IEEE80211_QOS_CHAR_PRES_* bits\n+ *\n+ * The whole element, header included, because a device takes it unchanged.\n+ */\n+struct ieee80211_qos_char_elem {\n+\tu8 element_id;\n+\tu8 length;\n+\tu8 element_id_ext;\n+\t__le32 control;\n+\t__le32 min_service_interval;\n+\t__le32 max_service_interval;\n+\tu8 min_data_rate[3];\n+\tu8 delay_bound[3];\n+\tu8 variable[];\n+} __packed;\n+\n+#define IEEE80211_QOS_CHAR_CTRL_DIRECTION\t\tGENMASK(1, 0)\n+#define IEEE80211_QOS_CHAR_CTRL_PRESENCE\t\tGENMASK(24, 9)\n+\n+#define IEEE80211_QOS_CHAR_DIR_UPLINK\t\t\t0\n+#define IEEE80211_QOS_CHAR_DIR_DOWNLINK\t\t\t1\n+#define IEEE80211_QOS_CHAR_DIR_DIRECT\t\t\t2\n+\n+#define IEEE80211_QOS_CHAR_PRES_MAX_MSDU_SIZE\t\tBIT(0)\n+#define IEEE80211_QOS_CHAR_PRES_SERVICE_START_TIME\tBIT(1)\n+#define IEEE80211_QOS_CHAR_PRES_SERVICE_START_LINK_ID\tBIT(2)\n+#define IEEE80211_QOS_CHAR_PRES_MEAN_DATA_RATE\t\tBIT(3)\n+#define IEEE80211_QOS_CHAR_PRES_BURST_SIZE\t\tBIT(4)\n+#define IEEE80211_QOS_CHAR_PRES_MSDU_LIFETIME\t\tBIT(5)\n+#define IEEE80211_QOS_CHAR_PRES_MSDU_DELIVERY_INFO\tBIT(6)\n+#define IEEE80211_QOS_CHAR_PRES_MEDIUM_TIME\t\tBIT(7)\n+\n+/**\n+ * ieee80211_qos_char_presence - QoS Characteristics presence bitmap\n+ * @qc: the element\n+ * Return: the Presence Bitmap Of Additional Parameters subfield, a bitmap of\n+ *\t%IEEE80211_QOS_CHAR_PRES_*\n+ */\n+static inline u16\n+ieee80211_qos_char_presence(const struct ieee80211_qos_char_elem *qc)\n+{\n+\treturn u32_get_bits(le32_to_cpu(qc-\u003econtrol),\n+\t\t\t IEEE80211_QOS_CHAR_CTRL_PRESENCE);\n+}\n+\n+/**\n+ * ieee80211_qos_char_direction - QoS Characteristics direction\n+ * @qc: the element\n+ * Return: %IEEE80211_QOS_CHAR_DIR_UPLINK, _DOWNLINK or _DIRECT\n+ */\n+static inline u8\n+ieee80211_qos_char_direction(const struct ieee80211_qos_char_elem *qc)\n+{\n+\treturn u32_get_bits(le32_to_cpu(qc-\u003econtrol),\n+\t\t\t IEEE80211_QOS_CHAR_CTRL_DIRECTION);\n+}\n+\n+/**\n+ * ieee80211_qos_char_size_ok - check a QoS Characteristics element\n+ * @data: candidate octets, from the Element ID\n+ * @len: length of @data\n+ *\n+ * Call this before the other accessors, which assume a valid element.\n+ *\n+ * Return: %true if @data holds a QoS Characteristics element whose length\n+ *\tmatches its presence bitmap\n+ */\n+static inline bool ieee80211_qos_char_size_ok(const u8 *data, size_t len)\n+{\n+\tconst struct ieee80211_qos_char_elem *qc = (const void *)data;\n+\tsize_t needed = sizeof(*qc);\n+\tu16 present;\n+\n+\tif (len \u003c needed)\n+\t\treturn false;\n+\n+\tif (qc-\u003eelement_id != WLAN_EID_EXTENSION ||\n+\t qc-\u003eelement_id_ext != WLAN_EID_EXT_QOS_CHARACTERISTICS ||\n+\t qc-\u003elength != len - 2)\n+\t\treturn false;\n+\n+\tpresent = ieee80211_qos_char_presence(qc);\n+\n+\tif (present \u0026 ~GENMASK(7, 0))\n+\t\treturn false;\n+\n+\tif (ieee80211_qos_char_direction(qc) \u003e IEEE80211_QOS_CHAR_DIR_DIRECT)\n+\t\treturn false;\n+\n+\t/* The LinkID refers to the Service Start Time, which must be present */\n+\tif (present \u0026 IEEE80211_QOS_CHAR_PRES_SERVICE_START_LINK_ID \u0026\u0026\n+\t !(present \u0026 IEEE80211_QOS_CHAR_PRES_SERVICE_START_TIME))\n+\t\treturn false;\n+\n+\tif (present \u0026 IEEE80211_QOS_CHAR_PRES_MAX_MSDU_SIZE)\n+\t\tneeded += 2;\n+\tif (present \u0026 IEEE80211_QOS_CHAR_PRES_SERVICE_START_TIME)\n+\t\tneeded += 4;\n+\tif (present \u0026 IEEE80211_QOS_CHAR_PRES_SERVICE_START_LINK_ID)\n+\t\tneeded += 1;\n+\tif (present \u0026 IEEE80211_QOS_CHAR_PRES_MEAN_DATA_RATE)\n+\t\tneeded += 3;\n+\tif (present \u0026 IEEE80211_QOS_CHAR_PRES_BURST_SIZE)\n+\t\tneeded += 4;\n+\tif (present \u0026 IEEE80211_QOS_CHAR_PRES_MSDU_LIFETIME)\n+\t\tneeded += 2;\n+\tif (present \u0026 IEEE80211_QOS_CHAR_PRES_MSDU_DELIVERY_INFO)\n+\t\tneeded += 1;\n+\tif (present \u0026 IEEE80211_QOS_CHAR_PRES_MEDIUM_TIME)\n+\t\tneeded += 2;\n+\n+\treturn len == needed;\n+}\n+\n #endif /* LINUX_IEEE80211_EHT_H */\ndiff --git a/include/linux/ieee80211.h b/include/linux/ieee80211.h\nindex 7fbc51f873ff1..4990db36cd540 100644\n--- a/include/linux/ieee80211.h\n+++ b/include/linux/ieee80211.h\n@@ -1501,6 +1501,9 @@ enum ieee80211_statuscode {\n \tWLAN_STATUS_NO_DIRECT_LINK = 48,\n \tWLAN_STATUS_STA_NOT_PRESENT = 49,\n \tWLAN_STATUS_STA_NOT_QSTA = 50,\n+\tWLAN_STATUS_REQUESTED_TCLAS_NOT_SUPPORTED_BY_AP = 56,\n+\tWLAN_STATUS_INSUFFICIENT_TCLAS_PROCESSING_RESOURCES = 57,\n+\tWLAN_STATUS_TCLAS_PROCESSING_TERMINATED = 97,\n \t/* 802.11s */\n \tWLAN_STATUS_ANTI_CLOG_REQUIRED = 76,\n \tWLAN_STATUS_FCG_NOT_SUPP = 78,\n@@ -1821,10 +1824,12 @@ enum ieee80211_eid_ext {\n \tWLAN_EID_EXT_SHORT_SSID_LIST = 58,\n \tWLAN_EID_EXT_HE_6GHZ_CAPA = 59,\n \tWLAN_EID_EXT_UL_MU_POWER_CAPA = 60,\n+\tWLAN_EID_EXT_TCLAS_MASK = 89,\n \tWLAN_EID_EXT_EHT_OPERATION = 106,\n \tWLAN_EID_EXT_EHT_MULTI_LINK = 107,\n \tWLAN_EID_EXT_EHT_CAPABILITY = 108,\n \tWLAN_EID_EXT_TID_TO_LINK_MAPPING = 109,\n+\tWLAN_EID_EXT_QOS_CHARACTERISTICS = 113,\n \tWLAN_EID_EXT_BANDWIDTH_INDICATION = 135,\n \tWLAN_EID_EXT_KNOWN_STA_IDENTIFCATION = 136,\n \tWLAN_EID_EXT_NON_AP_STA_REG_CON = 137,\ndiff --git a/include/net/cfg80211.h b/include/net/cfg80211.h\nindex 9aa7d2d4a1841..58b28c53a86b6 100644\n--- a/include/net/cfg80211.h\n+++ b/include/net/cfg80211.h\n@@ -4069,6 +4069,249 @@ struct cfg80211_qos_map {\n \tstruct cfg80211_dscp_range up[8];\n };\n \n+/**\n+ * enum cfg80211_tclas_processing - how the TCLAS elements relate\n+ *\n+ * @CFG80211_TCLAS_PROCESSING_ALL: an MSDU has to match every element\n+ * @CFG80211_TCLAS_PROCESSING_ANY: one matching element is enough\n+ * @CFG80211_TCLAS_PROCESSING_DEFAULT: matches every MSDU that no other\n+ *\tdescriptor matches, and carries no element\n+ * @CFG80211_TCLAS_PROCESSING_ABSENT: the descriptor carries no TCLAS\n+ *\tProcessing element. Not a value of the Processing field.\n+ */\n+enum cfg80211_tclas_processing {\n+\tCFG80211_TCLAS_PROCESSING_ALL\t\t= 0,\n+\tCFG80211_TCLAS_PROCESSING_ANY\t\t= 1,\n+\tCFG80211_TCLAS_PROCESSING_DEFAULT\t= 2,\n+\n+\tCFG80211_TCLAS_PROCESSING_ABSENT\t= 0xff,\n+};\n+\n+/**\n+ * enum cfg80211_flow_field - classifier parameter of one MSDU\n+ *\n+ * A set of these is held as a bitmap of BIT(field) in a u32.\n+ *\n+ * @CFG80211_FLOW_F_ETH_SA: Ethernet source address\n+ * @CFG80211_FLOW_F_ETH_DA: Ethernet destination address\n+ * @CFG80211_FLOW_F_ETH_TYPE: ethertype\n+ * @CFG80211_FLOW_F_VLAN_PCP: IEEE 802.1Q priority code point\n+ * @CFG80211_FLOW_F_VLAN_DEI: IEEE 802.1Q drop eligibility indicator\n+ * @CFG80211_FLOW_F_VLAN_VID: IEEE 802.1Q VLAN identifier\n+ * @CFG80211_FLOW_F_IP_VERSION: IP version\n+ * @CFG80211_FLOW_F_IP_SRC: IP source address\n+ * @CFG80211_FLOW_F_IP_DST: IP destination address\n+ * @CFG80211_FLOW_F_SRC_PORT: layer 4 source port\n+ * @CFG80211_FLOW_F_DST_PORT: layer 4 destination port\n+ * @CFG80211_FLOW_F_DSCP: differentiated services code point\n+ * @CFG80211_FLOW_F_PROTO: IPv4 protocol or IPv6 next header\n+ * @CFG80211_FLOW_F_FLOW_LABEL: IPv6 flow label\n+ * @NUM_CFG80211_FLOW_FIELDS: number of fields\n+ */\n+enum cfg80211_flow_field {\n+\tCFG80211_FLOW_F_ETH_SA,\n+\tCFG80211_FLOW_F_ETH_DA,\n+\tCFG80211_FLOW_F_ETH_TYPE,\n+\tCFG80211_FLOW_F_VLAN_PCP,\n+\tCFG80211_FLOW_F_VLAN_DEI,\n+\tCFG80211_FLOW_F_VLAN_VID,\n+\tCFG80211_FLOW_F_IP_VERSION,\n+\tCFG80211_FLOW_F_IP_SRC,\n+\tCFG80211_FLOW_F_IP_DST,\n+\tCFG80211_FLOW_F_SRC_PORT,\n+\tCFG80211_FLOW_F_DST_PORT,\n+\tCFG80211_FLOW_F_DSCP,\n+\tCFG80211_FLOW_F_PROTO,\n+\tCFG80211_FLOW_F_FLOW_LABEL,\n+\n+\tNUM_CFG80211_FLOW_FIELDS,\n+};\n+\n+/**\n+ * struct cfg80211_flow_key - masked classifier parameters of one MSDU\n+ *\n+ * Unselected fields are zero. The whole structure is hashed and compared as a\n+ * byte string, so it must contain no undefined octet.\n+ *\n+ * @src: IP source address, IPv4 in the first four octets\n+ * @dst: IP destination address, IPv4 in the first four octets\n+ * @flow_label: IPv6 flow label\n+ * @sa: Ethernet source address\n+ * @da: Ethernet destination address\n+ * @eth_type: ethertype\n+ * @vlan_tci: IEEE 802.1Q tag control information\n+ * @src_port: layer 4 source port\n+ * @dst_port: layer 4 destination port\n+ * @ip_version: IP version, which keeps IPv4 and IPv6 keys apart\n+ * @dscp: differentiated services code point, in the six LSBs\n+ * @proto: IPv4 protocol or IPv6 next header\n+ * @pad: must be zero\n+ */\n+struct cfg80211_flow_key {\n+\tstruct in6_addr src;\n+\tstruct in6_addr dst;\n+\t__be32 flow_label;\n+\tu8 sa[ETH_ALEN];\n+\tu8 da[ETH_ALEN];\n+\t__be16 eth_type;\n+\t__be16 vlan_tci;\n+\t__be16 src_port;\n+\t__be16 dst_port;\n+\tu8 ip_version;\n+\tu8 dscp;\n+\tu8 proto;\n+\tu8 pad;\n+};\n+\n+/* Longest Filter Value and Filter Mask a filter classifier may carry */\n+#define CFG80211_TCLAS_FILTER_LEN\t28\n+\n+/**\n+ * struct cfg80211_tclas_filter - octet filter on a protocol payload\n+ *\n+ * @protocol: IPv4 protocol or IPv6 next header value that locates the\n+ *\tpayload to compare\n+ * @instance: which occurrence of @protocol to use, counted from zero\n+ * @len: length of @value and @mask\n+ * @value: octets to compare against the payload\n+ * @mask: the bits of @value that are compared\n+ */\n+struct cfg80211_tclas_filter {\n+\tu8 protocol;\n+\tu8 instance;\n+\tu8 len;\n+\tu8 value[CFG80211_TCLAS_FILTER_LEN];\n+\tu8 mask[CFG80211_TCLAS_FILTER_LEN];\n+};\n+\n+/**\n+ * struct cfg80211_tclas - parsed TCLAS element\n+ *\n+ * Only the classifier types the kernel can evaluate are represented, see\n+ * cfg80211_parse_tclas().\n+ *\n+ * @type: classifier type\n+ * @fields: the parameters the classifier mask selects, as a bitmap of\n+ *\t\u0026enum cfg80211_flow_field. Zero for a filter classifier, which\n+ *\tselects no field of its own.\n+ * @key: the required values, with unselected fields zero, so that a match is\n+ *\ta comparison of two keys\n+ * @filter: the octet filter, when @type is a filter classifier\n+ */\n+struct cfg80211_tclas {\n+\tu8 type;\n+\tu32 fields;\n+\tunion {\n+\t\tstruct cfg80211_flow_key key;\n+\t\tstruct cfg80211_tclas_filter filter;\n+\t};\n+};\n+\n+/**\n+ * struct cfg80211_flow_info - classifier parameters of one MSDU\n+ *\n+ * @key: every parameter the source carries, with nothing masked out\n+ * @present: bitmap of \u0026enum cfg80211_flow_field, the parameters the source\n+ *\tactually carries. A classifier that names an absent parameter cannot\n+ *\tmatch.\n+ * @skb: the frame the parameters came from, or %NULL when the caller\n+ *\tdescribes a flow rather than a frame. A filter classifier reads the\n+ *\tframe again and cannot be evaluated without it.\n+ * @l3_off: offset of the IP header in @skb\n+ */\n+struct cfg80211_flow_info {\n+\tstruct cfg80211_flow_key key;\n+\tu32 present;\n+\tconst struct sk_buff *skb;\n+\tu16 l3_off;\n+};\n+\n+bool cfg80211_flow_parse(struct sk_buff *skb, struct cfg80211_flow_info *info);\n+\n+/**\n+ * enum cfg80211_flow_dir - direction of a built flow key\n+ *\n+ * @CFG80211_FLOW_AS_IS: the key describes the frame that was parsed\n+ * @CFG80211_FLOW_MIRRORED: the key describes the reverse direction, with the\n+ *\taddresses and the ports swapped\n+ */\n+enum cfg80211_flow_dir {\n+\tCFG80211_FLOW_AS_IS,\n+\tCFG80211_FLOW_MIRRORED,\n+};\n+\n+bool cfg80211_flow_key_build(const struct cfg80211_flow_info *info, u32 fields,\n+\t\t\t enum cfg80211_flow_dir dir,\n+\t\t\t struct cfg80211_flow_key *key);\n+\n+/**\n+ * struct cfg80211_scs_desc - one SCS descriptor\n+ *\n+ * @req_type: add, remove or change\n+ * @tclas_processing: how the elements of @tclas relate\n+ * @qos_char: the whole QoS Characteristics element, or %NULL. Set only\n+ *\tafter ieee80211_qos_char_size_ok() passed, so the accessors in\n+ *\t\u003clinux/ieee80211-eht.h\u003e may read it.\n+ * @id: SCSID, 1 to 255\n+ * @up: user priority to assign to a matching MSDU\n+ * @qos_char_len: length of @qos_char\n+ * @n_tclas: number of entries in @tclas\n+ * @tclas: the classifier\n+ */\n+struct cfg80211_scs_desc {\n+\tenum nl80211_scs_req_type req_type;\n+\tenum cfg80211_tclas_processing tclas_processing;\n+\tconst struct ieee80211_qos_char_elem *qos_char;\n+\tu8 id;\n+\tu8 up;\n+\tu16 qos_char_len;\n+\tu8 n_tclas;\n+\tstruct cfg80211_tclas tclas[] __counted_by(n_tclas);\n+};\n+\n+/**\n+ * struct cfg80211_scs_result - result for one SCS descriptor\n+ *\n+ * @status: IEEE status code\n+ */\n+struct cfg80211_scs_result {\n+\tu16 status;\n+};\n+\n+/**\n+ * struct cfg80211_mscs_desc - the MSCS of one peer\n+ *\n+ * @req_type: add, remove or change\n+ * @up_bitmap: user priorities that the AP learns from, one bit each\n+ * @up_limit: ceiling for the assigned user priority, 0 to 7\n+ * @stream_timeout: minimum lifetime of a learned value, in TUs\n+ * @fields: classifier parameters, a bitmap of \u0026enum cfg80211_flow_field\n+ */\n+struct cfg80211_mscs_desc {\n+\tenum nl80211_scs_req_type req_type;\n+\tu8 up_bitmap;\n+\tu8 up_limit;\n+\tu32 stream_timeout;\n+\tu32 fields;\n+};\n+\n+/**\n+ * struct cfg80211_scs_verdict - result of an SCS evaluation\n+ *\n+ * @match: an SCS descriptor matched the MSDU\n+ * @scsid: identifier of the matching descriptor\n+ * @up: user priority to assign\n+ */\n+struct cfg80211_scs_verdict {\n+\tbool match;\n+\tu8 scsid;\n+\tu8 up;\n+};\n+\n+void cfg80211_scs_evaluate(struct cfg80211_scs_desc * const *desc, u8 n_desc,\n+\t\t\t const struct cfg80211_flow_info *info,\n+\t\t\t struct cfg80211_scs_verdict *verdict);\n+\n /**\n * DOC: Neighbor Awareness Networking (NAN)\n *\n@@ -5154,6 +5397,13 @@ struct mgmt_frame_regs {\n *\n * @set_qos_map: Set QoS mapping information to the driver\n *\n+ * @set_scs: Add, change or remove SCS descriptors of one peer. Userspace\n+ *\thandles the action frame exchange. Each descriptor is answered with\n+ *\ta status code in the result entry of the same index. A declined add\n+ *\tor change leaves the previous classification of that SCSID in force.\n+ *\tA negative return fails the request as a whole.\n+ * @set_mscs: Install, change or remove the MSCS of one peer.\n+ *\n * @set_ap_chanwidth: Set the AP (including P2P GO) mode channel width for the\n *\tgiven interface This is used e.g. for dynamic HT 20/40 MHz channel width\n *\tchanges during the lifetime of the BSS.\n@@ -5558,6 +5808,13 @@ struct cfg80211_ops {\n \t\t\t struct net_device *dev,\n \t\t\t struct cfg80211_qos_map *qos_map);\n \n+\tint\t(*set_scs)(struct wiphy *wiphy, struct net_device *dev,\n+\t\t\t const u8 *peer,\n+\t\t\t struct cfg80211_scs_desc * const *desc,\n+\t\t\t struct cfg80211_scs_result *res, u8 n_desc);\n+\tint\t(*set_mscs)(struct wiphy *wiphy, struct net_device *dev,\n+\t\t\t const u8 *peer, struct cfg80211_mscs_desc *desc);\n+\n \tint\t(*set_ap_chanwidth)(struct wiphy *wiphy, struct net_device *dev,\n \t\t\t\t unsigned int link_id,\n \t\t\t\t struct cfg80211_chan_def *chandef);\ndiff --git a/include/net/mac80211.h b/include/net/mac80211.h\nindex d6f3c58559101..c65ebbb3265c3 100644\n--- a/include/net/mac80211.h\n+++ b/include/net/mac80211.h\n@@ -5082,6 +5082,34 @@ struct ieee80211_ops {\n \t\t\t struct cfg80211_pmsr_request *request);\n \tvoid (*abort_pmsr)(struct ieee80211_hw *hw, struct ieee80211_vif *vif,\n \t\t\t struct cfg80211_pmsr_request *request);\n+\t/**\n+\t * @sta_set_scs:\n+\t * Called with the SCS descriptors that a station requested, after\n+\t * cfg80211 validated them. Optional, only reached when the wiphy\n+\t * advertises %NL80211_EXT_FEATURE_SCS. Without it, every descriptor\n+\t * with a QoS Characteristics element is declined.\n+\t *\n+\t * - The results arrive zeroed, which means accepted. Write only\n+\t * refusals, in the result entry of the same index.\n+\t * - Decline a descriptor only when its traffic description cannot\n+\t * be served. mac80211 classifies frames in software either way.\n+\t * - A declined add or change keeps the previous descriptor of that\n+\t * SCSID. An accepted one without a traffic description replaces\n+\t * the device state of that SCSID.\n+\t * - A removal only drops driver state, the result is ignored. A\n+\t * station going away drops its rules without a call.\n+\t * - After a hardware restart every stored rule is passed again as\n+\t * an add, one per call, and the result is ignored.\n+\t * - A negative return fails the whole request and leaves the rule\n+\t * set unchanged, so the driver must leave the device as it was.\n+\t * - The descriptors belong to the MLD, not to one link.\n+\t *\n+\t * Called with the wiphy mutex held. This callback may sleep.\n+\t */\n+\tint (*sta_set_scs)(struct ieee80211_hw *hw, struct ieee80211_vif *vif,\n+\t\t\t struct ieee80211_sta *sta,\n+\t\t\t struct cfg80211_scs_desc * const *desc,\n+\t\t\t struct cfg80211_scs_result *res, u8 n_desc);\n \tint (*set_tid_config)(struct ieee80211_hw *hw,\n \t\t\t struct ieee80211_vif *vif,\n \t\t\t struct ieee80211_sta *sta,\ndiff --git a/include/uapi/linux/nl80211.h b/include/uapi/linux/nl80211.h\nindex 9a2ccb8d66b86..ecb11739c5b7d 100644\n--- a/include/uapi/linux/nl80211.h\n+++ b/include/uapi/linux/nl80211.h\n@@ -1427,6 +1427,18 @@\n * @NL80211_CMD_STOP_PD: Stop the PD operation, identified by\n *\tits %NL80211_ATTR_WDEV interface.\n *\n+ * @NL80211_CMD_SET_SCS: Add, change or remove stream classification service\n+ *\tdescriptors of the peer given by %NL80211_ATTR_MAC, as requested in an\n+ *\tSCS Request frame. The descriptors are in\n+ *\t%NL80211_ATTR_SCS_DESCRIPTORS. The reply repeats the attribute with\n+ *\tthe identifier and the status code of each descriptor. A declined\n+ *\tdescriptor does not make the command fail.\n+ * @NL80211_CMD_SET_MSCS: Install, change or remove the mirrored stream\n+ *\tclassification service of the peer given by %NL80211_ATTR_MAC, as\n+ *\trequested in an MSCS Request frame or a (Re)Association Request. The\n+ *\tparameters are in %NL80211_ATTR_MSCS_DESCRIPTOR. The reply carries\n+ *\tthe status code in %NL80211_ATTR_STATUS_CODE.\n+ *\n * @NL80211_CMD_MAX: highest used command number\n * @__NL80211_CMD_AFTER_LAST: internal use\n */\n@@ -1705,6 +1717,9 @@ enum nl80211_commands {\n \tNL80211_CMD_START_PD,\n \tNL80211_CMD_STOP_PD,\n \n+\tNL80211_CMD_SET_SCS,\n+\tNL80211_CMD_SET_MSCS,\n+\n \t/* add new commands above here */\n \n \t/* used to define NL80211_CMD_MAX below */\n@@ -3190,6 +3205,12 @@ enum nl80211_commands {\n *\tknown station to transmit a frame. This is relevant to know whether\n *\tMLD address translation happened or to disable it when sending a frame.\n *\n+ * @NL80211_ATTR_SCS_DESCRIPTORS: Nested array of SCS descriptors, each one a\n+ *\tnested set of \u0026enum nl80211_scs_desc_attr attributes. Used with\n+ *\t%NL80211_CMD_SET_SCS in both directions.\n+ * @NL80211_ATTR_MSCS_DESCRIPTOR: Nested set of \u0026enum nl80211_mscs_desc_attr\n+ *\tattributes. Used with %NL80211_CMD_SET_MSCS.\n+ *\n * @NUM_NL80211_ATTR: total number of nl80211_attrs available\n * @NL80211_ATTR_MAX: highest attribute number currently defined\n * @__NL80211_ATTR_AFTER_LAST: internal use\n@@ -3792,6 +3813,9 @@ enum nl80211_attrs {\n \n \tNL80211_ATTR_FRAME_NO_STA,\n \n+\tNL80211_ATTR_SCS_DESCRIPTORS,\n+\tNL80211_ATTR_MSCS_DESCRIPTOR,\n+\n \t/* add attributes here, update the policy in nl80211.c */\n \n \t__NL80211_ATTR_AFTER_LAST,\n@@ -6077,6 +6101,87 @@ enum nl80211_tx_power_setting {\n \tNL80211_TX_POWER_FIXED,\n };\n \n+/**\n+ * enum nl80211_scs_req_type - SCS or MSCS request type\n+ *\n+ * The values match the Request Type field of the descriptor on the air.\n+ *\n+ * @NL80211_SCS_REQ_ADD: create the stream, replacing an active one of the\n+ *\tsame identifier\n+ * @NL80211_SCS_REQ_REMOVE: terminate the stream\n+ * @NL80211_SCS_REQ_CHANGE: modify an active stream, leaving it in force if\n+ *\tthe request is declined\n+ */\n+enum nl80211_scs_req_type {\n+\tNL80211_SCS_REQ_ADD,\n+\tNL80211_SCS_REQ_REMOVE,\n+\tNL80211_SCS_REQ_CHANGE,\n+};\n+\n+/**\n+ * enum nl80211_scs_desc_attr - one SCS descriptor\n+ *\n+ * @__NL80211_SCS_DESC_ATTR_INVALID: invalid\n+ * @NL80211_SCS_DESC_ATTR_ID: SCSID (u8, 1 to 255)\n+ * @NL80211_SCS_DESC_ATTR_REQ_TYPE: request type (u8, see\n+ *\t\u0026enum nl80211_scs_req_type)\n+ * @NL80211_SCS_DESC_ATTR_UP: user priority to assign to a matching MSDU\n+ *\t(u8, 0 to 7), from the Intra-Access Category Priority element.\n+ *\tRequired for a downlink descriptor or one without\n+ *\t%NL80211_SCS_DESC_ATTR_QOS_CHAR, and rejected otherwise.\n+ * @NL80211_SCS_DESC_ATTR_TCLAS: one or more TCLAS elements and at most one\n+ *\tTCLAS Processing element (binary), as received over the air\n+ * @NL80211_SCS_DESC_ATTR_QOS_CHAR: the whole QoS Characteristics element\n+ *\t(binary), header included, as received over the air\n+ * @NL80211_SCS_DESC_ATTR_STATUS: IEEE status code (u16), kernel to userspace\n+ *\n+ * @__NL80211_SCS_DESC_ATTR_AFTER_LAST: internal use\n+ * @NL80211_SCS_DESC_ATTR_MAX: highest attribute\n+ */\n+enum nl80211_scs_desc_attr {\n+\t__NL80211_SCS_DESC_ATTR_INVALID,\n+\tNL80211_SCS_DESC_ATTR_ID,\n+\tNL80211_SCS_DESC_ATTR_REQ_TYPE,\n+\tNL80211_SCS_DESC_ATTR_UP,\n+\tNL80211_SCS_DESC_ATTR_TCLAS,\n+\tNL80211_SCS_DESC_ATTR_QOS_CHAR,\n+\tNL80211_SCS_DESC_ATTR_STATUS,\n+\n+\t__NL80211_SCS_DESC_ATTR_AFTER_LAST,\n+\tNL80211_SCS_DESC_ATTR_MAX = __NL80211_SCS_DESC_ATTR_AFTER_LAST - 1\n+};\n+\n+/**\n+ * enum nl80211_mscs_desc_attr - the MSCS of one peer\n+ *\n+ * @__NL80211_MSCS_DESC_ATTR_INVALID: invalid\n+ * @NL80211_MSCS_DESC_ATTR_REQ_TYPE: request type (u8, see\n+ *\t\u0026enum nl80211_scs_req_type)\n+ * @NL80211_MSCS_DESC_ATTR_UP_BITMAP: user priorities that the AP learns\n+ *\tfrom, one bit each (u8)\n+ * @NL80211_MSCS_DESC_ATTR_UP_LIMIT: ceiling for the assigned user priority\n+ *\t(u8, 0 to 7)\n+ * @NL80211_MSCS_DESC_ATTR_STREAM_TIMEOUT: minimum lifetime of a learned\n+ *\tvalue, in TUs (u32)\n+ * @NL80211_MSCS_DESC_ATTR_TCLAS_MASK: one or more TCLAS Mask elements\n+ *\t(binary), as received over the air. Required for add and change,\n+ *\trejected for remove.\n+ *\n+ * @__NL80211_MSCS_DESC_ATTR_AFTER_LAST: internal use\n+ * @NL80211_MSCS_DESC_ATTR_MAX: highest attribute\n+ */\n+enum nl80211_mscs_desc_attr {\n+\t__NL80211_MSCS_DESC_ATTR_INVALID,\n+\tNL80211_MSCS_DESC_ATTR_REQ_TYPE,\n+\tNL80211_MSCS_DESC_ATTR_UP_BITMAP,\n+\tNL80211_MSCS_DESC_ATTR_UP_LIMIT,\n+\tNL80211_MSCS_DESC_ATTR_STREAM_TIMEOUT,\n+\tNL80211_MSCS_DESC_ATTR_TCLAS_MASK,\n+\n+\t__NL80211_MSCS_DESC_ATTR_AFTER_LAST,\n+\tNL80211_MSCS_DESC_ATTR_MAX = __NL80211_MSCS_DESC_ATTR_AFTER_LAST - 1\n+};\n+\n /**\n * enum nl80211_tid_config - TID config state\n * @NL80211_TID_CONFIG_ENABLE: Enable config for the TID\n@@ -7125,6 +7230,10 @@ enum nl80211_feature_flags {\n * @NL80211_EXT_FEATURE_FAST_ROAM_OFFLOAD: Driver supports fast roaming\n *\toffload in station mode, including Fast Transition or Opportunistic\n *\tKey Caching.\n+ * @NL80211_EXT_FEATURE_SCS: Driver supports the stream classification\n+ *\tservice, configured with %NL80211_CMD_SET_SCS.\n+ * @NL80211_EXT_FEATURE_MSCS: Driver supports the mirrored stream\n+ *\tclassification service, configured with %NL80211_CMD_SET_MSCS.\n *\n * @NUM_NL80211_EXT_FEATURES: number of extended features.\n * @MAX_NL80211_EXT_FEATURES: highest extended feature index.\n@@ -7209,6 +7318,8 @@ enum nl80211_ext_feature_index {\n \tNL80211_EXT_FEATURE_SET_KEY_LTF_SEED,\n \tNL80211_EXT_FEATURE_PROBE_AP,\n \tNL80211_EXT_FEATURE_FAST_ROAM_OFFLOAD,\n+\tNL80211_EXT_FEATURE_SCS,\n+\tNL80211_EXT_FEATURE_MSCS,\n \n \t/* add new features before the definition below */\n \tNUM_NL80211_EXT_FEATURES,\ndiff --git a/net/mac80211/Makefile b/net/mac80211/Makefile\nindex 36f1e3e2222b4..8f8729e44c5f1 100644\n--- a/net/mac80211/Makefile\n+++ b/net/mac80211/Makefile\n@@ -35,7 +35,8 @@ mac80211-y := \\\n \ttdls.o \\\n \tocb.o \\\n \tairtime.o \\\n-\teht.o uhr.o nan.o\n+\teht.o uhr.o nan.o \\\n+\tscs.o\n \n mac80211-$(CONFIG_MAC80211_LEDS) += led.o\n mac80211-$(CONFIG_MAC80211_DEBUGFS) += \\\ndiff --git a/net/mac80211/cfg.c b/net/mac80211/cfg.c\nindex a1753335eb9d0..04c5947718c3c 100644\n--- a/net/mac80211/cfg.c\n+++ b/net/mac80211/cfg.c\n@@ -6073,6 +6073,8 @@ const struct cfg80211_ops mac80211_config_ops = {\n \t.end_cac = ieee80211_end_cac,\n \t.channel_switch = ieee80211_channel_switch,\n \t.set_qos_map = ieee80211_set_qos_map,\n+\t.set_scs = ieee80211_set_scs,\n+\t.set_mscs = ieee80211_set_mscs,\n \t.set_ap_chanwidth = ieee80211_set_ap_chanwidth,\n \t.add_tx_ts = ieee80211_add_tx_ts,\n \t.del_tx_ts = ieee80211_del_tx_ts,\ndiff --git a/net/mac80211/debugfs_sta.c b/net/mac80211/debugfs_sta.c\nindex ef75255d47d5a..bcf4961af24f4 100644\n--- a/net/mac80211/debugfs_sta.c\n+++ b/net/mac80211/debugfs_sta.c\n@@ -10,6 +10,7 @@\n \n #include \u003clinux/debugfs.h\u003e\n #include \u003clinux/ieee80211.h\u003e\n+#include \u003clinux/if_vlan.h\u003e\n #include \"ieee80211_i.h\"\n #include \"debugfs.h\"\n #include \"debugfs_sta.h\"\n@@ -131,6 +132,243 @@ static ssize_t sta_last_seq_ctrl_read(struct file *file, char __user *userbuf,\n }\n STA_OPS(last_seq_ctrl);\n \n+static void sta_flow_key_show(struct seq_file *s,\n+\t\t\t const struct cfg80211_flow_key *key, u32 fields)\n+{\n+\tu16 tci = be16_to_cpu(key-\u003evlan_tci);\n+\n+\tif (fields \u0026 BIT(CFG80211_FLOW_F_ETH_SA))\n+\t\tseq_printf(s, \" sa=%pM\", key-\u003esa);\n+\tif (fields \u0026 BIT(CFG80211_FLOW_F_ETH_DA))\n+\t\tseq_printf(s, \" da=%pM\", key-\u003eda);\n+\tif (fields \u0026 BIT(CFG80211_FLOW_F_ETH_TYPE))\n+\t\tseq_printf(s, \" ethertype=0x%04x\", be16_to_cpu(key-\u003eeth_type));\n+\n+\tif (fields \u0026 BIT(CFG80211_FLOW_F_VLAN_PCP))\n+\t\tseq_printf(s, \" pcp=%u\",\n+\t\t\t (tci \u0026 VLAN_PRIO_MASK) \u003e\u003e VLAN_PRIO_SHIFT);\n+\tif (fields \u0026 BIT(CFG80211_FLOW_F_VLAN_DEI))\n+\t\tseq_printf(s, \" dei=%u\", !!(tci \u0026 VLAN_CFI_MASK));\n+\tif (fields \u0026 BIT(CFG80211_FLOW_F_VLAN_VID))\n+\t\tseq_printf(s, \" vid=%u\", tci \u0026 VLAN_VID_MASK);\n+\n+\tif (fields \u0026 BIT(CFG80211_FLOW_F_IP_VERSION))\n+\t\tseq_printf(s, \" ipv%u\", key-\u003eip_version);\n+\n+\tif (fields \u0026 BIT(CFG80211_FLOW_F_IP_SRC))\n+\t\tseq_printf(s, key-\u003eip_version == 6 ? \" src=%pI6\" : \" src=%pI4\",\n+\t\t\t \u0026key-\u003esrc);\n+\tif (fields \u0026 BIT(CFG80211_FLOW_F_IP_DST))\n+\t\tseq_printf(s, key-\u003eip_version == 6 ? \" dst=%pI6\" : \" dst=%pI4\",\n+\t\t\t \u0026key-\u003edst);\n+\n+\tif (fields \u0026 BIT(CFG80211_FLOW_F_SRC_PORT))\n+\t\tseq_printf(s, \" sport=%u\", be16_to_cpu(key-\u003esrc_port));\n+\tif (fields \u0026 BIT(CFG80211_FLOW_F_DST_PORT))\n+\t\tseq_printf(s, \" dport=%u\", be16_to_cpu(key-\u003edst_port));\n+\n+\tif (fields \u0026 BIT(CFG80211_FLOW_F_DSCP))\n+\t\tseq_printf(s, \" dscp=%u\", key-\u003edscp);\n+\tif (fields \u0026 BIT(CFG80211_FLOW_F_PROTO))\n+\t\tseq_printf(s, \" proto=%u\", key-\u003eproto);\n+\tif (fields \u0026 BIT(CFG80211_FLOW_F_FLOW_LABEL))\n+\t\tseq_printf(s, \" flowlabel=0x%05x\",\n+\t\t\t be32_to_cpu(key-\u003eflow_label));\n+}\n+\n+/* The set can be replaced during a walk, so look it up again at every step */\n+static void *sta_scs_seq_start(struct seq_file *s, loff_t *pos)\n+\t__acquires(RCU)\n+{\n+\tstruct sta_info *sta = s-\u003eprivate;\n+\tstruct ieee80211_scs_sta *scs;\n+\n+\trcu_read_lock();\n+\tscs = rcu_dereference(sta-\u003escs);\n+\tif (!scs || *pos \u003e= scs-\u003en_rules)\n+\t\treturn NULL;\n+\n+\treturn scs-\u003erule[*pos];\n+}\n+\n+static void *sta_scs_seq_next(struct seq_file *s, void *v, loff_t *pos)\n+{\n+\tstruct sta_info *sta = s-\u003eprivate;\n+\tstruct ieee80211_scs_sta *scs = rcu_dereference(sta-\u003escs);\n+\n+\t++*pos;\n+\tif (!scs || *pos \u003e= scs-\u003en_rules)\n+\t\treturn NULL;\n+\n+\treturn scs-\u003erule[*pos];\n+}\n+\n+static void sta_scs_seq_stop(struct seq_file *s, void *v)\n+\t__releases(RCU)\n+{\n+\trcu_read_unlock();\n+}\n+\n+static int sta_scs_seq_show(struct seq_file *s, void *v)\n+{\n+\tstatic const char * const processing[] = {\n+\t\t[CFG80211_TCLAS_PROCESSING_ALL] = \"all\",\n+\t\t[CFG80211_TCLAS_PROCESSING_ANY] = \"any\",\n+\t\t[CFG80211_TCLAS_PROCESSING_DEFAULT] = \"default\",\n+\t};\n+\tconst struct cfg80211_scs_desc *desc = v;\n+\tconst char *proc = \"none\";\n+\tu8 i;\n+\n+\tif (desc-\u003etclas_processing \u003c ARRAY_SIZE(processing))\n+\t\tproc = processing[desc-\u003etclas_processing];\n+\n+\tseq_printf(s, \"scsid %u up %u processing %s\", desc-\u003eid, desc-\u003eup, proc);\n+\n+\tif (desc-\u003eqos_char)\n+\t\tseq_printf(s, \" qos_char %u direction %u\", desc-\u003eqos_char_len,\n+\t\t\t ieee80211_qos_char_direction(desc-\u003eqos_char));\n+\n+\tseq_putc(s, '\\n');\n+\n+\tfor (i = 0; i \u003c desc-\u003en_tclas; i++) {\n+\t\tconst struct cfg80211_tclas *t = \u0026desc-\u003etclas[i];\n+\n+\t\tseq_printf(s, \" type %u\", t-\u003etype);\n+\t\tsta_flow_key_show(s, \u0026t-\u003ekey, t-\u003efields);\n+\t\tseq_putc(s, '\\n');\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static const struct seq_operations sta_scs_seq_ops = {\n+\t.start = sta_scs_seq_start,\n+\t.next = sta_scs_seq_next,\n+\t.stop = sta_scs_seq_stop,\n+\t.show = sta_scs_seq_show,\n+};\n+\n+static int sta_scs_open(struct inode *inode, struct file *file)\n+{\n+\tstruct seq_file *s;\n+\tint ret;\n+\n+\tret = seq_open(file, \u0026sta_scs_seq_ops);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\ts = file-\u003eprivate_data;\n+\ts-\u003eprivate = inode-\u003ei_private;\n+\n+\treturn 0;\n+}\n+\n+static const struct file_operations sta_scs_ops = {\n+\t.open = sta_scs_open,\n+\t.read = seq_read,\n+\t.llseek = seq_lseek,\n+\t.release = seq_release,\n+};\n+\n+struct sta_mscs_iter {\n+\tstruct sta_info *sta;\n+\tstruct ieee80211_mscs_sta *mscs;\n+};\n+\n+/* The entry list is not an RCU list, so the whole walk holds the lock */\n+static void *sta_mscs_seq_start(struct seq_file *s, loff_t *pos)\n+\t__acquires(RCU)\n+{\n+\tstruct sta_mscs_iter *iter = s-\u003eprivate;\n+\n+\trcu_read_lock();\n+\titer-\u003emscs = rcu_dereference(iter-\u003esta-\u003emscs);\n+\tif (!iter-\u003emscs)\n+\t\treturn NULL;\n+\n+\tspin_lock_bh(\u0026iter-\u003emscs-\u003elock);\n+\n+\tif (*pos == 0)\n+\t\treturn SEQ_START_TOKEN;\n+\n+\treturn seq_list_start(\u0026iter-\u003emscs-\u003eentries, *pos - 1);\n+}\n+\n+static void *sta_mscs_seq_next(struct seq_file *s, void *v, loff_t *pos)\n+{\n+\tstruct sta_mscs_iter *iter = s-\u003eprivate;\n+\n+\tif (v == SEQ_START_TOKEN) {\n+\t\t++*pos;\n+\t\treturn seq_list_start(\u0026iter-\u003emscs-\u003eentries, 0);\n+\t}\n+\n+\treturn seq_list_next(v, \u0026iter-\u003emscs-\u003eentries, pos);\n+}\n+\n+static void sta_mscs_seq_stop(struct seq_file *s, void *v)\n+\t__releases(RCU)\n+{\n+\tstruct sta_mscs_iter *iter = s-\u003eprivate;\n+\n+\tif (iter-\u003emscs)\n+\t\tspin_unlock_bh(\u0026iter-\u003emscs-\u003elock);\n+\n+\trcu_read_unlock();\n+}\n+\n+static int sta_mscs_seq_show(struct seq_file *s, void *v)\n+{\n+\tstruct sta_mscs_iter *iter = s-\u003eprivate;\n+\tstruct ieee80211_mscs_sta *mscs = iter-\u003emscs;\n+\tstruct ieee80211_flow_entry *entry;\n+\n+\tif (v == SEQ_START_TOKEN) {\n+\t\tseq_printf(s,\n+\t\t\t \"up_bitmap 0x%02x up_limit %u timeout %ums entries %u\\n\",\n+\t\t\t mscs-\u003eup_bitmap, mscs-\u003eup_limit,\n+\t\t\t jiffies_to_msecs(mscs-\u003etimeout), mscs-\u003en_entries);\n+\t\treturn 0;\n+\t}\n+\n+\tentry = list_entry(v, struct ieee80211_flow_entry, list);\n+\n+\tseq_printf(s, \" up %u age %ums\", READ_ONCE(entry-\u003eup),\n+\t\t jiffies_to_msecs(jiffies - READ_ONCE(entry-\u003elast_update)));\n+\tsta_flow_key_show(s, \u0026entry-\u003ehkey.key, mscs-\u003elayout);\n+\tseq_putc(s, '\\n');\n+\n+\treturn 0;\n+}\n+\n+static const struct seq_operations sta_mscs_seq_ops = {\n+\t.start = sta_mscs_seq_start,\n+\t.next = sta_mscs_seq_next,\n+\t.stop = sta_mscs_seq_stop,\n+\t.show = sta_mscs_seq_show,\n+};\n+\n+static int sta_mscs_open(struct inode *inode, struct file *file)\n+{\n+\tstruct sta_mscs_iter *iter;\n+\n+\titer = __seq_open_private(file, \u0026sta_mscs_seq_ops, sizeof(*iter));\n+\tif (!iter)\n+\t\treturn -ENOMEM;\n+\n+\titer-\u003esta = inode-\u003ei_private;\n+\n+\treturn 0;\n+}\n+\n+static const struct file_operations sta_mscs_ops = {\n+\t.open = sta_mscs_open,\n+\t.read = seq_read,\n+\t.llseek = seq_lseek,\n+\t.release = seq_release_private,\n+};\n+\n #define AQM_TXQ_ENTRY_LEN 130\n \n static ssize_t sta_aqm_read(struct file *file, char __user *userbuf,\n@@ -1252,6 +1490,8 @@ void ieee80211_sta_debugfs_add(struct sta_info *sta)\n \tsta-\u003edebugfs_dir = debugfs_create_dir(mac, stations_dir);\n \n \tDEBUGFS_ADD(flags);\n+\tDEBUGFS_ADD(scs);\n+\tDEBUGFS_ADD(mscs);\n \tDEBUGFS_ADD(aid);\n \tDEBUGFS_ADD(num_ps_buf_frames);\n \tDEBUGFS_ADD(last_seq_ctrl);\ndiff --git a/net/mac80211/driver-ops.h b/net/mac80211/driver-ops.h\nindex f1c0b87fddd5f..f7684e8db0251 100644\n--- a/net/mac80211/driver-ops.h\n+++ b/net/mac80211/driver-ops.h\n@@ -1552,6 +1552,32 @@ static inline void drv_del_nan_func(struct ieee80211_local *local,\n \ttrace_drv_return_void(local);\n }\n \n+static inline int drv_sta_set_scs(struct ieee80211_local *local,\n+\t\t\t\t struct ieee80211_sub_if_data *sdata,\n+\t\t\t\t struct sta_info *sta,\n+\t\t\t\t struct cfg80211_scs_desc * const *desc,\n+\t\t\t\t struct cfg80211_scs_result *res, u8 n_desc)\n+{\n+\tint ret;\n+\n+\tmight_sleep();\n+\tlockdep_assert_wiphy(local-\u003ehw.wiphy);\n+\n+\tif (!local-\u003eops-\u003esta_set_scs || !sta-\u003euploaded)\n+\t\treturn 0;\n+\n+\tsdata = get_bss_sdata(sdata);\n+\tif (!check_sdata_in_driver(sdata))\n+\t\treturn -EIO;\n+\n+\ttrace_drv_sta_set_scs(local, sdata, \u0026sta-\u003esta, n_desc);\n+\tret = local-\u003eops-\u003esta_set_scs(\u0026local-\u003ehw, \u0026sdata-\u003evif, \u0026sta-\u003esta, desc,\n+\t\t\t\t res, n_desc);\n+\ttrace_drv_return_int(local, ret);\n+\n+\treturn ret;\n+}\n+\n static inline int drv_set_tid_config(struct ieee80211_local *local,\n \t\t\t\t struct ieee80211_sub_if_data *sdata,\n \t\t\t\t struct ieee80211_sta *sta,\ndiff --git a/net/mac80211/ieee80211_i.h b/net/mac80211/ieee80211_i.h\nindex 10767666c45c0..e0a7e9bb75a0d 100644\n--- a/net/mac80211/ieee80211_i.h\n+++ b/net/mac80211/ieee80211_i.h\n@@ -345,6 +345,10 @@ struct ieee80211_if_ap {\n \n \tbool multicast_to_unicast;\n \tbool active;\n+\n+\t/* learned MSCS entries of every station of this BSS, and its VLANs */\n+\tstruct rhashtable flow_tbl;\n+\tstruct wiphy_delayed_work flow_gc_work;\n };\n \n struct ieee80211_if_vlan {\n@@ -2214,6 +2218,19 @@ void ieee80211_recalc_txpower(struct ieee80211_link_data *link,\n \t\t\t bool update_bss);\n void ieee80211_recalc_offload(struct ieee80211_local *local);\n \n+/* SCS and MSCS */\n+int ieee80211_set_scs(struct wiphy *wiphy, struct net_device *dev,\n+\t\t const u8 *peer, struct cfg80211_scs_desc * const *desc,\n+\t\t struct cfg80211_scs_result *res, u8 n_desc);\n+int ieee80211_set_mscs(struct wiphy *wiphy, struct net_device *dev,\n+\t\t const u8 *peer, struct cfg80211_mscs_desc *desc);\n+void ieee80211_sta_scs_reconfig(struct sta_info *sta);\n+void ieee80211_sta_scs_free(struct sta_info *sta);\n+bool ieee80211_flow_classify(struct sta_info *sta, struct sk_buff *skb);\n+void ieee80211_flow_learn(struct ieee80211_rx_data *rx);\n+int ieee80211_flow_tbl_init(struct ieee80211_sub_if_data *sdata);\n+void ieee80211_flow_tbl_destroy(struct ieee80211_sub_if_data *sdata);\n+\n /* link handling */\n void ieee80211_link_setup(struct ieee80211_link_data *link);\n void ieee80211_link_init(struct ieee80211_sub_if_data *sdata,\ndiff --git a/net/mac80211/iface.c b/net/mac80211/iface.c\nindex 8535dd478633c..7e1f97e1bfcd3 100644\n--- a/net/mac80211/iface.c\n+++ b/net/mac80211/iface.c\n@@ -542,6 +542,8 @@ static void ieee80211_do_stop(struct ieee80211_sub_if_data *sdata, bool going_do\n \t\tatomic_dec(\u0026local-\u003eiff_allmultis);\n \n \tif (sdata-\u003evif.type == NL80211_IFTYPE_AP) {\n+\t\tieee80211_flow_tbl_destroy(sdata);\n+\n \t\tlocal-\u003efif_pspoll--;\n \t\tlocal-\u003efif_probe_req--;\n \t} else if (sdata-\u003evif.type == NL80211_IFTYPE_ADHOC) {\n@@ -1435,6 +1437,10 @@ int ieee80211_do_open(struct wireless_dev *wdev, bool coming_up)\n \t\tbreak;\n \t\t}\n \tcase NL80211_IFTYPE_AP:\n+\t\tres = ieee80211_flow_tbl_init(sdata);\n+\t\tif (res)\n+\t\t\treturn res;\n+\t\tbreak;\n \tcase NL80211_IFTYPE_MESH_POINT:\n \tcase NL80211_IFTYPE_STATION:\n \tcase NL80211_IFTYPE_MONITOR:\n@@ -1469,7 +1475,7 @@ int ieee80211_do_open(struct wireless_dev *wdev, bool coming_up)\n \t\t\t * no need to worry about AP_VLAN/NAN_DATA cleanup since\n \t\t\t * in that case we can't have open_count == 0\n \t\t\t */\n-\t\t\treturn res;\n+\t\t\tgoto err_flow_tbl;\n \t\t}\n \t\tieee80211_led_radio(local, true);\n \t\tieee80211_mod_tpt_led_trig(local,\n@@ -1645,6 +1651,9 @@ int ieee80211_do_open(struct wireless_dev *wdev, bool coming_up)\n \t\tlist_del(\u0026sdata-\u003eu.vlan.list);\n \t/* Might not be initialized yet, but it is harmless */\n \tsdata-\u003ebss = NULL;\n+ err_flow_tbl:\n+\tif (sdata-\u003evif.type == NL80211_IFTYPE_AP)\n+\t\tieee80211_flow_tbl_destroy(sdata);\n \treturn res;\n }\n \ndiff --git a/net/mac80211/rx.c b/net/mac80211/rx.c\nindex b3990b7a72990..fb7450715d2d8 100644\n--- a/net/mac80211/rx.c\n+++ b/net/mac80211/rx.c\n@@ -2790,6 +2790,9 @@ ieee80211_deliver_skb(struct ieee80211_rx_data *rx)\n \t\tu64_stats_update_end(\u0026rx-\u003elink_sta-\u003erx_stats.syncp);\n \t}\n \n+\tif (rx-\u003esta)\n+\t\tieee80211_flow_learn(rx);\n+\n \tif ((sdata-\u003evif.type == NL80211_IFTYPE_AP ||\n \t sdata-\u003evif.type == NL80211_IFTYPE_AP_VLAN) \u0026\u0026\n \t !(sdata-\u003eflags \u0026 IEEE80211_SDATA_DONT_BRIDGE_PACKETS) \u0026\u0026\n@@ -4995,6 +4998,8 @@ static void ieee80211_rx_8023(struct ieee80211_rx_data *rx,\n \tu64_stats_add(\u0026stats-\u003ebytes, orig_len);\n \tu64_stats_update_end(\u0026stats-\u003esyncp);\n \n+\tieee80211_flow_learn(rx);\n+\n \tif (fast_rx-\u003einternal_forward) {\n \t\tstruct sk_buff *xmit_skb = NULL;\n \t\tif (is_multicast_ether_addr(da)) {\ndiff --git a/net/mac80211/scs.c b/net/mac80211/scs.c\nnew file mode 100644\nindex 0000000000000..076e97cddbf1a\n--- /dev/null\n+++ b/net/mac80211/scs.c\n@@ -0,0 +1,574 @@\n+// SPDX-License-Identifier: GPL-2.0-only\n+/*\n+ * Stream classification service (SCS) and mirrored SCS (MSCS)\n+ *\n+ * Copyright (C) 2026 Felix Fietkau \u003cnbd@nbd.name\u003e\n+ */\n+#include \u003clinux/ieee80211.h\u003e\n+#include \u003cnet/cfg80211.h\u003e\n+#include \"driver-ops.h\"\n+#include \"ieee80211_i.h\"\n+#include \"sta_info.h\"\n+\n+/* An hour, in TUs of 1024 microseconds */\n+#define IEEE80211_MSCS_TIMEOUT_MAX_TU\t3515625\n+\n+/* Limit on learned MSCS entries per station */\n+#define IEEE80211_MSCS_ENTRIES_MAX\t64\n+\n+/* The hashed bytes must hold no padding between the peer and the key */\n+static_assert(offsetofend(struct ieee80211_flow_hkey, key) ==\n+\t sizeof(struct sta_info *) + sizeof(struct cfg80211_flow_key));\n+\n+static const struct rhashtable_params flow_rht_params = {\n+\t.head_offset = offsetof(struct ieee80211_flow_entry, node),\n+\t.key_offset = offsetof(struct ieee80211_flow_entry, hkey),\n+\t.key_len = offsetofend(struct ieee80211_flow_hkey, key),\n+\t.automatic_shrinking = true,\n+};\n+\n+static size_t ieee80211_scs_rule_size(const struct cfg80211_scs_desc *desc)\n+{\n+\treturn ALIGN(struct_size(desc, tclas, desc-\u003en_tclas) +\n+\t\t desc-\u003eqos_char_len, __alignof__(*desc));\n+}\n+\n+static void *ieee80211_scs_rule_copy(const struct cfg80211_scs_desc *src,\n+\t\t\t\t void *pos)\n+{\n+\tsize_t len = struct_size(src, tclas, src-\u003en_tclas);\n+\tstruct cfg80211_scs_desc *dst = pos;\n+\n+\tmemcpy(dst, src, len);\n+\t/* A stored rule is always handed back to the driver as an install */\n+\tdst-\u003ereq_type = NL80211_SCS_REQ_ADD;\n+\tpos += len;\n+\n+\tif (src-\u003eqos_char_len) {\n+\t\tmemcpy(pos, src-\u003eqos_char, src-\u003eqos_char_len);\n+\t\tdst-\u003eqos_char = pos;\n+\t} else {\n+\t\tdst-\u003eqos_char = NULL;\n+\t}\n+\n+\treturn PTR_ALIGN(pos + src-\u003eqos_char_len, __alignof__(*dst));\n+}\n+\n+static u8 ieee80211_scs_rule_find(struct cfg80211_scs_desc * const *rule,\n+\t\t\t\t u8 n_rules, u8 id)\n+{\n+\tu8 i;\n+\n+\tfor (i = 0; i \u003c n_rules; i++)\n+\t\tif (rule[i]-\u003eid == id)\n+\t\t\tbreak;\n+\n+\treturn i;\n+}\n+\n+static size_t ieee80211_scs_sta_head(unsigned int n_rules)\n+{\n+\tstruct ieee80211_scs_sta *scs;\n+\n+\treturn ALIGN(struct_size(scs, rule, n_rules),\n+\t\t __alignof__(struct cfg80211_scs_desc));\n+}\n+\n+/*\n+ * Size for every rule that can end up in the set, so that no allocation\n+ * fails after the driver programmed the accepted descriptors.\n+ */\n+static size_t ieee80211_scs_sta_size(const struct ieee80211_scs_sta *old,\n+\t\t\t\t struct cfg80211_scs_desc * const *desc,\n+\t\t\t\t u8 n_desc, unsigned int n_alloc)\n+{\n+\tsize_t size = ieee80211_scs_sta_head(n_alloc);\n+\tunsigned int i;\n+\n+\tfor (i = 0; old \u0026\u0026 i \u003c old-\u003en_rules; i++)\n+\t\tsize += ieee80211_scs_rule_size(old-\u003erule[i]);\n+\n+\tfor (i = 0; i \u003c n_desc; i++)\n+\t\tsize += ieee80211_scs_rule_size(desc[i]);\n+\n+\treturn size;\n+}\n+\n+/*\n+ * @rule points into the old set and into the request, so copy each rule.\n+ * The block was sized for @n_alloc rules.\n+ */\n+static void ieee80211_scs_sta_fill(struct ieee80211_scs_sta *scs,\n+\t\t\t\t struct cfg80211_scs_desc * const *rule,\n+\t\t\t\t u8 n_rules, unsigned int n_alloc)\n+{\n+\tvoid *pos = (void *)scs + ieee80211_scs_sta_head(n_alloc);\n+\tu8 i;\n+\n+\t/* Set before the array is filled, for __counted_by() */\n+\tscs-\u003en_rules = n_rules;\n+\n+\tfor (i = 0; i \u003c n_rules; i++) {\n+\t\tscs-\u003erule[i] = pos;\n+\t\tpos = ieee80211_scs_rule_copy(rule[i], pos);\n+\t}\n+}\n+\n+/**\n+ * ieee80211_flow_classify - give an MSDU the user priority of its stream\n+ *\n+ * @sta: the receiver\n+ * @skb: the MSDU, in IEEE 802.3 format\n+ *\n+ * Return: %true when it set skb-\u003epriority, %false to leave the frame to the\n+ *\tQoS map.\n+ */\n+bool ieee80211_flow_classify(struct sta_info *sta, struct sk_buff *skb)\n+{\n+\tstruct ieee80211_flow_entry *entry;\n+\tstruct cfg80211_scs_verdict verdict;\n+\tstruct ieee80211_mscs_sta *mscs;\n+\tstruct ieee80211_flow_hkey hkey;\n+\tstruct cfg80211_flow_info info;\n+\tstruct ieee80211_scs_sta *scs;\n+\n+\tscs = rcu_dereference(sta-\u003escs);\n+\tmscs = rcu_dereference(sta-\u003emscs);\n+\tif (!scs \u0026\u0026 !mscs)\n+\t\treturn false;\n+\n+\tif (!cfg80211_flow_parse(skb, \u0026info))\n+\t\treturn false;\n+\n+\tif (scs) {\n+\t\tcfg80211_scs_evaluate(scs-\u003erule, scs-\u003en_rules, \u0026info,\n+\t\t\t\t \u0026verdict);\n+\t\tif (verdict.match) {\n+\t\t\tskb-\u003epriority = verdict.up;\n+\n+\t\t\treturn true;\n+\t\t}\n+\t}\n+\n+\t/* An SCS match takes precedence over the MSCS */\n+\tif (!mscs)\n+\t\treturn false;\n+\n+\thkey.sta = sta;\n+\tif (!cfg80211_flow_key_build(\u0026info, mscs-\u003elayout, CFG80211_FLOW_AS_IS,\n+\t\t\t\t \u0026hkey.key))\n+\t\treturn false;\n+\n+\tentry = rhashtable_lookup(\u0026sta-\u003esdata-\u003ebss-\u003eflow_tbl, \u0026hkey,\n+\t\t\t\t flow_rht_params);\n+\tif (!entry)\n+\t\treturn false;\n+\n+\tskb-\u003epriority = min_t(u8, READ_ONCE(entry-\u003eup), mscs-\u003eup_limit);\n+\n+\treturn true;\n+}\n+\n+static void ieee80211_flow_entry_free(struct ieee80211_if_ap *ap,\n+\t\t\t\t struct ieee80211_flow_entry *entry)\n+{\n+\trhashtable_remove_fast(\u0026ap-\u003eflow_tbl, \u0026entry-\u003enode, flow_rht_params);\n+\tlist_del(\u0026entry-\u003elist);\n+\tkfree_rcu(entry, rcu_head);\n+}\n+\n+/* The caller holds @mscs-\u003elock unless nothing else can reach the list */\n+static void ieee80211_flow_purge(struct ieee80211_if_ap *ap,\n+\t\t\t\t struct ieee80211_mscs_sta *mscs)\n+{\n+\tstruct ieee80211_flow_entry *entry, *tmp;\n+\n+\tlist_for_each_entry_safe(entry, tmp, \u0026mscs-\u003eentries, list)\n+\t\tieee80211_flow_entry_free(ap, entry);\n+\n+\tmscs-\u003en_entries = 0;\n+}\n+\n+/* Return the time until the next entry expires, zero when none is left */\n+static unsigned long ieee80211_flow_gc_sta(struct ieee80211_if_ap *ap,\n+\t\t\t\t\t struct sta_info *sta)\n+{\n+\tstruct ieee80211_flow_entry *entry, *tmp;\n+\tstruct ieee80211_mscs_sta *mscs;\n+\tunsigned long delay = 0;\n+\n+\tmscs = wiphy_dereference(sta-\u003elocal-\u003ehw.wiphy, sta-\u003emscs);\n+\tif (!mscs)\n+\t\treturn 0;\n+\n+\tspin_lock_bh(\u0026mscs-\u003elock);\n+\n+\tlist_for_each_entry_safe(entry, tmp, \u0026mscs-\u003eentries, list) {\n+\t\tunsigned long dead = READ_ONCE(entry-\u003elast_update) +\n+\t\t\t\t mscs-\u003etimeout;\n+\n+\t\tif (time_after_eq(jiffies, dead)) {\n+\t\t\tieee80211_flow_entry_free(ap, entry);\n+\t\t\tmscs-\u003en_entries--;\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\tif (!delay || time_before(dead, jiffies + delay))\n+\t\t\tdelay = dead - jiffies;\n+\t}\n+\n+\tspin_unlock_bh(\u0026mscs-\u003elock);\n+\n+\treturn delay;\n+}\n+\n+static void ieee80211_flow_gc_work(struct wiphy *wiphy, struct wiphy_work *work)\n+{\n+\tstruct ieee80211_sub_if_data *sdata;\n+\tstruct ieee80211_if_ap *ap;\n+\tunsigned long delay = 0;\n+\tstruct sta_info *sta;\n+\n+\tap = container_of(work, struct ieee80211_if_ap, flow_gc_work.work);\n+\tsdata = container_of(ap, struct ieee80211_sub_if_data, u.ap);\n+\n+\tlist_for_each_entry(sta, \u0026sdata-\u003elocal-\u003esta_list, list) {\n+\t\tunsigned long sta_delay;\n+\n+\t\tif (sta-\u003esdata-\u003ebss != ap)\n+\t\t\tcontinue;\n+\n+\t\tsta_delay = ieee80211_flow_gc_sta(ap, sta);\n+\t\tif (sta_delay \u0026\u0026 (!delay || sta_delay \u003c delay))\n+\t\t\tdelay = sta_delay;\n+\t}\n+\n+\tif (delay)\n+\t\twiphy_delayed_work_queue(wiphy, \u0026ap-\u003eflow_gc_work, delay);\n+}\n+\n+int ieee80211_flow_tbl_init(struct ieee80211_sub_if_data *sdata)\n+{\n+\twiphy_delayed_work_init(\u0026sdata-\u003eu.ap.flow_gc_work,\n+\t\t\t\tieee80211_flow_gc_work);\n+\n+\treturn rhashtable_init(\u0026sdata-\u003eu.ap.flow_tbl, \u0026flow_rht_params);\n+}\n+\n+static void ieee80211_flow_tbl_free(void *ptr, void *arg)\n+{\n+\tstruct ieee80211_flow_entry *entry = ptr;\n+\n+\tkfree(entry);\n+}\n+\n+void ieee80211_flow_tbl_destroy(struct ieee80211_sub_if_data *sdata)\n+{\n+\twiphy_delayed_work_cancel(sdata-\u003elocal-\u003ehw.wiphy,\n+\t\t\t\t \u0026sdata-\u003eu.ap.flow_gc_work);\n+\trhashtable_free_and_destroy(\u0026sdata-\u003eu.ap.flow_tbl,\n+\t\t\t\t ieee80211_flow_tbl_free, NULL);\n+}\n+\n+static void ieee80211_flow_insert(struct ieee80211_sub_if_data *sdata,\n+\t\t\t\t struct ieee80211_mscs_sta *mscs,\n+\t\t\t\t const struct ieee80211_flow_hkey *hkey, u8 up)\n+{\n+\tstruct ieee80211_flow_entry *entry, *other;\n+\tbool first;\n+\n+\tentry = kzalloc_obj(*entry, GFP_ATOMIC);\n+\tif (!entry)\n+\t\treturn;\n+\n+\tentry-\u003ehkey.sta = hkey-\u003esta;\n+\tentry-\u003ehkey.key = hkey-\u003ekey;\n+\tentry-\u003eup = up;\n+\tentry-\u003elast_update = jiffies;\n+\n+\tspin_lock_bh(\u0026mscs-\u003elock);\n+\n+\t/* Another MSCS may have been installed, which emptied this list */\n+\tif (rcu_dereference(hkey-\u003esta-\u003emscs) != mscs)\n+\t\tgoto drop;\n+\n+\tif (mscs-\u003en_entries \u003e= IEEE80211_MSCS_ENTRIES_MAX)\n+\t\tgoto drop;\n+\n+\tother = rhashtable_lookup_get_insert_fast(\u0026sdata-\u003ebss-\u003eflow_tbl,\n+\t\t\t\t\t\t \u0026entry-\u003enode,\n+\t\t\t\t\t\t flow_rht_params);\n+\tif (other) {\n+\t\t/* Concurrent insert of the same key, the last writer wins */\n+\t\tif (!IS_ERR(other)) {\n+\t\t\tWRITE_ONCE(other-\u003eup, up);\n+\t\t\tWRITE_ONCE(other-\u003elast_update, jiffies);\n+\t\t}\n+\n+\t\tgoto drop;\n+\t}\n+\n+\tlist_add_tail(\u0026entry-\u003elist, \u0026mscs-\u003eentries);\n+\tfirst = mscs-\u003en_entries++ == 0;\n+\tspin_unlock_bh(\u0026mscs-\u003elock);\n+\n+\t/* The work requeues itself while entries remain */\n+\tif (first)\n+\t\twiphy_delayed_work_queue(sdata-\u003elocal-\u003ehw.wiphy,\n+\t\t\t\t\t \u0026sdata-\u003ebss-\u003eflow_gc_work, 1);\n+\n+\treturn;\n+\n+drop:\n+\tspin_unlock_bh(\u0026mscs-\u003elock);\n+\tkfree(entry);\n+}\n+\n+/**\n+ * ieee80211_flow_learn - record the user priority of an uplink flow\n+ *\n+ * @rx: the received frame, in IEEE 802.3 format\n+ *\n+ * The key is built for the downlink direction, so that the transmit path\n+ * can look it up as is.\n+ */\n+void ieee80211_flow_learn(struct ieee80211_rx_data *rx)\n+{\n+\tstruct ieee80211_sub_if_data *sdata = rx-\u003esdata;\n+\tconst struct ethhdr *eth = (void *)rx-\u003eskb-\u003edata;\n+\tstruct ieee80211_flow_hkey hkey;\n+\tstruct ieee80211_mscs_sta *mscs;\n+\tstruct ieee80211_flow_entry *entry;\n+\tstruct cfg80211_flow_info info;\n+\t/* The TID from ieee80211_parse_qos(), or from a decap offload driver */\n+\tu32 up = rx-\u003eskb-\u003epriority;\n+\n+\tif (sdata-\u003evif.type != NL80211_IFTYPE_AP \u0026\u0026\n+\t sdata-\u003evif.type != NL80211_IFTYPE_AP_VLAN)\n+\t\treturn;\n+\n+\tmscs = rcu_dereference(rx-\u003esta-\u003emscs);\n+\tif (!mscs)\n+\t\treturn;\n+\n+\tif (up \u003e 7 || !(mscs-\u003eup_bitmap \u0026 BIT(up)))\n+\t\treturn;\n+\n+\tif (is_multicast_ether_addr(eth-\u003eh_dest) ||\n+\t !ether_addr_equal(eth-\u003eh_source, rx-\u003esta-\u003eaddr))\n+\t\treturn;\n+\n+\tif (!cfg80211_flow_parse(rx-\u003eskb, \u0026info))\n+\t\treturn;\n+\n+\thkey.sta = rx-\u003esta;\n+\tif (!cfg80211_flow_key_build(\u0026info, mscs-\u003elayout,\n+\t\t\t\t CFG80211_FLOW_MIRRORED, \u0026hkey.key))\n+\t\treturn;\n+\n+\tentry = rhashtable_lookup(\u0026sdata-\u003ebss-\u003eflow_tbl, \u0026hkey,\n+\t\t\t\t flow_rht_params);\n+\tif (entry) {\n+\t\tWRITE_ONCE(entry-\u003eup, up);\n+\t\tWRITE_ONCE(entry-\u003elast_update, jiffies);\n+\t\treturn;\n+\t}\n+\n+\tieee80211_flow_insert(sdata, mscs, \u0026hkey, up);\n+}\n+\n+int ieee80211_set_scs(struct wiphy *wiphy, struct net_device *dev,\n+\t\t const u8 *peer, struct cfg80211_scs_desc * const *desc,\n+\t\t struct cfg80211_scs_result *res, u8 n_desc)\n+{\n+\tstruct ieee80211_sub_if_data *sdata = IEEE80211_DEV_TO_SUB_IF(dev);\n+\tstruct ieee80211_scs_sta *scs, *old;\n+\tstruct cfg80211_scs_desc **rule;\n+\tstruct sta_info *sta;\n+\tunsigned int n_alloc;\n+\tu8 i, n_rules = 0;\n+\tint ret;\n+\n+\tlockdep_assert_wiphy(wiphy);\n+\n+\tsta = sta_info_get_bss(sdata, peer);\n+\tif (!sta)\n+\t\treturn -ENOENT;\n+\n+\told = wiphy_dereference(wiphy, sta-\u003escs);\n+\n+\t/* One rule per SCSID bounds the size of the new set */\n+\tn_alloc = (old ? old-\u003en_rules : 0) + n_desc;\n+\n+\trule = kcalloc(n_alloc, sizeof(*rule), GFP_KERNEL);\n+\tif (!rule)\n+\t\treturn -ENOMEM;\n+\n+\t/* 255 rules with 255 classifiers each exceed what kmalloc hands out */\n+\tscs = kvzalloc(ieee80211_scs_sta_size(old, desc, n_desc, n_alloc),\n+\t\t GFP_KERNEL);\n+\tif (!scs) {\n+\t\tret = -ENOMEM;\n+\t\tgoto free;\n+\t}\n+\n+\tif (sdata-\u003elocal-\u003eops-\u003esta_set_scs \u0026\u0026 sta-\u003euploaded) {\n+\t\tret = drv_sta_set_scs(sdata-\u003elocal, sdata, sta, desc, res,\n+\t\t\t\t n_desc);\n+\t\tif (ret)\n+\t\t\tgoto free;\n+\t} else {\n+\t\t/* Without a driver, no traffic description can be served */\n+\t\tfor (i = 0; i \u003c n_desc; i++)\n+\t\t\tif (desc[i]-\u003eqos_char)\n+\t\t\t\tres[i].status = WLAN_STATUS_REQUEST_DECLINED;\n+\t}\n+\n+\tif (old) {\n+\t\tn_rules = old-\u003en_rules;\n+\t\tmemcpy(rule, old-\u003erule, n_rules * sizeof(*rule));\n+\t}\n+\n+\tfor (i = 0; i \u003c n_desc; i++) {\n+\t\tu8 at = ieee80211_scs_rule_find(rule, n_rules, desc[i]-\u003eid);\n+\n+\t\tif (desc[i]-\u003ereq_type == NL80211_SCS_REQ_REMOVE) {\n+\t\t\t/* A removal is answered even when no rule matched */\n+\t\t\tres[i].status = WLAN_STATUS_TCLAS_PROCESSING_TERMINATED;\n+\n+\t\t\tif (at \u003c n_rules) {\n+\t\t\t\tn_rules--;\n+\t\t\t\tmemmove(\u0026rule[at], \u0026rule[at + 1],\n+\t\t\t\t\t(n_rules - at) * sizeof(*rule));\n+\t\t\t}\n+\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\tif (res[i].status != WLAN_STATUS_SUCCESS)\n+\t\t\tcontinue;\n+\n+\t\tif (at \u003c n_rules)\n+\t\t\trule[at] = desc[i];\n+\t\telse\n+\t\t\trule[n_rules++] = desc[i];\n+\t}\n+\n+\tif (n_rules) {\n+\t\tieee80211_scs_sta_fill(scs, rule, n_rules, n_alloc);\n+\t} else {\n+\t\tkvfree(scs);\n+\t\tscs = NULL;\n+\t}\n+\n+\tkfree(rule);\n+\n+\trcu_assign_pointer(sta-\u003escs, scs);\n+\tif (old)\n+\t\tkvfree_rcu(old, rcu_head);\n+\n+\treturn 0;\n+\n+free:\n+\tkvfree(scs);\n+\tkfree(rule);\n+\n+\treturn ret;\n+}\n+\n+int ieee80211_set_mscs(struct wiphy *wiphy, struct net_device *dev,\n+\t\t const u8 *peer, struct cfg80211_mscs_desc *desc)\n+{\n+\tstruct ieee80211_sub_if_data *sdata = IEEE80211_DEV_TO_SUB_IF(dev);\n+\tstruct ieee80211_mscs_sta *mscs = NULL, *old;\n+\tstruct sta_info *sta;\n+\tu32 tu;\n+\n+\tlockdep_assert_wiphy(wiphy);\n+\n+\t/* Only an AP provides MSCS */\n+\tif (!sdata-\u003ebss)\n+\t\treturn -EOPNOTSUPP;\n+\n+\tsta = sta_info_get_bss(sdata, peer);\n+\tif (!sta)\n+\t\treturn -ENOENT;\n+\n+\tif (desc-\u003ereq_type != NL80211_SCS_REQ_REMOVE) {\n+\t\tmscs = kzalloc_obj(*mscs);\n+\t\tif (!mscs)\n+\t\t\treturn -ENOMEM;\n+\n+\t\tmscs-\u003elayout = desc-\u003efields;\n+\t\tmscs-\u003eup_bitmap = desc-\u003eup_bitmap;\n+\t\tmscs-\u003eup_limit = desc-\u003eup_limit;\n+\n+\t\t/*\n+\t\t * Cap at an hour to keep tu * 1024 within the unsigned int\n+\t\t * that usecs_to_jiffies() takes. A timeout of zero jiffies\n+\t\t * would expire an entry at the next collection.\n+\t\t */\n+\t\ttu = min(desc-\u003estream_timeout, IEEE80211_MSCS_TIMEOUT_MAX_TU);\n+\t\tmscs-\u003etimeout = max(1UL, usecs_to_jiffies(tu * 1024));\n+\n+\t\tspin_lock_init(\u0026mscs-\u003elock);\n+\t\tINIT_LIST_HEAD(\u0026mscs-\u003eentries);\n+\t}\n+\n+\told = wiphy_dereference(wiphy, sta-\u003emscs);\n+\trcu_assign_pointer(sta-\u003emscs, mscs);\n+\n+\tif (old) {\n+\t\t/* The entries were keyed with the old layout */\n+\t\tspin_lock_bh(\u0026old-\u003elock);\n+\t\tieee80211_flow_purge(sta-\u003esdata-\u003ebss, old);\n+\t\tspin_unlock_bh(\u0026old-\u003elock);\n+\n+\t\tkfree_rcu(old, rcu_head);\n+\t}\n+\n+\treturn 0;\n+}\n+\n+/*\n+ * A restarted device lost every stream, so program them again. A refusal\n+ * changes nothing here: the rule stays and mac80211 keeps classifying for it.\n+ */\n+void ieee80211_sta_scs_reconfig(struct sta_info *sta)\n+{\n+\tstruct ieee80211_local *local = sta-\u003elocal;\n+\tstruct ieee80211_scs_sta *scs;\n+\tu8 i;\n+\n+\tlockdep_assert_wiphy(local-\u003ehw.wiphy);\n+\n+\tscs = wiphy_dereference(local-\u003ehw.wiphy, sta-\u003escs);\n+\tif (!scs)\n+\t\treturn;\n+\n+\t/* One rule per call, so no result array has to be allocated */\n+\tfor (i = 0; i \u003c scs-\u003en_rules; i++) {\n+\t\tstruct cfg80211_scs_result res = {};\n+\n+\t\tdrv_sta_set_scs(local, sta-\u003esdata, sta, \u0026scs-\u003erule[i], \u0026res, 1);\n+\t}\n+}\n+\n+void ieee80211_sta_scs_free(struct sta_info *sta)\n+{\n+\tstruct ieee80211_mscs_sta *mscs;\n+\tstruct ieee80211_scs_sta *scs;\n+\n+\t/* Nothing can reach the station any more, so no lock is needed */\n+\tscs = rcu_dereference_raw(sta-\u003escs);\n+\tRCU_INIT_POINTER(sta-\u003escs, NULL);\n+\tkvfree(scs);\n+\n+\tmscs = rcu_dereference_raw(sta-\u003emscs);\n+\tRCU_INIT_POINTER(sta-\u003emscs, NULL);\n+\tif (!mscs)\n+\t\treturn;\n+\n+\tieee80211_flow_purge(sta-\u003esdata-\u003ebss, mscs);\n+\tkfree(mscs);\n+}\ndiff --git a/net/mac80211/sta_info.c b/net/mac80211/sta_info.c\nindex fdf00cbf49d8b..e14d78b002f1c 100644\n--- a/net/mac80211/sta_info.c\n+++ b/net/mac80211/sta_info.c\n@@ -508,6 +508,7 @@ void sta_info_free(struct ieee80211_local *local, struct sta_info *sta)\n \tkfree(sta-\u003emesh);\n #endif\n \n+\tieee80211_sta_scs_free(sta);\n \tsta_info_free_link(\u0026sta-\u003edeflink);\n \tkfree(sta);\n }\ndiff --git a/net/mac80211/sta_info.h b/net/mac80211/sta_info.h\nindex ee0d32877c5b4..6e65313fe4d9c 100644\n--- a/net/mac80211/sta_info.h\n+++ b/net/mac80211/sta_info.h\n@@ -634,6 +634,79 @@ struct ieee80211_sta_removed_link_stats {\n \t} pertid_stats;\n };\n \n+/**\n+ * struct ieee80211_scs_sta - the SCS rule set of one station\n+ *\n+ * @rcu_head: for freeing\n+ * @n_rules: number of entries in @rule\n+ * @rule: the active descriptors, in the order they were installed\n+ *\n+ * One allocation holds this structure, the descriptors it points to and their\n+ * QoS Characteristics octets, so the whole set has one lifetime.\n+ */\n+struct ieee80211_scs_sta {\n+\tstruct rcu_head rcu_head;\n+\tu8 n_rules;\n+\tstruct cfg80211_scs_desc *rule[] __counted_by(n_rules);\n+};\n+\n+/**\n+ * struct ieee80211_mscs_sta - the MSCS of one station\n+ *\n+ * @rcu_head: for freeing\n+ * @layout: classifier parameters, a bitmap of \u0026enum cfg80211_flow_field\n+ * @up_bitmap: user priorities that the AP learns from, one bit each\n+ * @up_limit: ceiling for the assigned user priority\n+ * @timeout: minimum lifetime of a learned value, in jiffies\n+ * @lock: protects @entries and @n_entries\n+ * @entries: the learned entries of this station, for teardown\n+ * @n_entries: number of entries in @entries\n+ */\n+struct ieee80211_mscs_sta {\n+\tstruct rcu_head rcu_head;\n+\tu32 layout;\n+\tu8 up_bitmap;\n+\tu8 up_limit;\n+\tunsigned long timeout;\n+\n+\tspinlock_t lock; /* protects @entries and @n_entries */\n+\tstruct list_head entries;\n+\tu32 n_entries;\n+};\n+\n+/**\n+ * struct ieee80211_flow_hkey - what a learned entry is looked up by\n+ *\n+ * @sta: the peer, so that a flow of one station never answers for another\n+ * @key: the masked tuple, in the downlink direction\n+ *\n+ * Not packed: the whole of it is compared, but a packed aggregate would force\n+ * byte-wise access to @sta.\n+ */\n+struct ieee80211_flow_hkey {\n+\tstruct sta_info *sta;\n+\tstruct cfg80211_flow_key key;\n+};\n+\n+/**\n+ * struct ieee80211_flow_entry - the user priority learned for one flow\n+ *\n+ * @hkey: what it is looked up by\n+ * @node: hash table linkage\n+ * @list: linkage in \u0026ieee80211_mscs_sta.entries\n+ * @last_update: jiffies of the last uplink frame of this flow\n+ * @up: the learned user priority\n+ * @rcu_head: for freeing\n+ */\n+struct ieee80211_flow_entry {\n+\tstruct ieee80211_flow_hkey hkey;\n+\tstruct rhash_head node;\n+\tstruct list_head list;\n+\tunsigned long last_update;\n+\tu8 up;\n+\tstruct rcu_head rcu_head;\n+};\n+\n /**\n * struct sta_info - STA information\n *\n@@ -694,6 +767,8 @@ struct ieee80211_sta_removed_link_stats {\n *\n * @fast_tx: TX fastpath information\n * @fast_rx: RX fastpath information\n+ * @scs: SCS rule set, or %NULL\n+ * @mscs: MSCS parameters, or %NULL\n * @tdls_chandef: a TDLS peer can have a wider chandef that is compatible to\n *\tthe BSS one.\n * @frags: fragment cache\n@@ -730,6 +805,9 @@ struct sta_info {\n \tstruct ieee80211_fast_tx __rcu *fast_tx;\n \tstruct ieee80211_fast_rx __rcu *fast_rx;\n \n+\tstruct ieee80211_scs_sta __rcu *scs;\n+\tstruct ieee80211_mscs_sta __rcu *mscs;\n+\n #ifdef CONFIG_MAC80211_MESH\n \tstruct mesh_sta *mesh;\n #endif\ndiff --git a/net/mac80211/trace.h b/net/mac80211/trace.h\nindex 562a4964afa3a..1e6026306fb2e 100644\n--- a/net/mac80211/trace.h\n+++ b/net/mac80211/trace.h\n@@ -1023,6 +1023,33 @@ DECLARE_EVENT_CLASS(sta_event,\n \t)\n );\n \n+TRACE_EVENT(drv_sta_set_scs,\n+\tTP_PROTO(struct ieee80211_local *local,\n+\t\t struct ieee80211_sub_if_data *sdata,\n+\t\t struct ieee80211_sta *sta, u8 n_desc),\n+\n+\tTP_ARGS(local, sdata, sta, n_desc),\n+\n+\tTP_STRUCT__entry(\n+\t\tLOCAL_ENTRY\n+\t\tVIF_ENTRY\n+\t\tSTA_ENTRY\n+\t\t__field(u8, n_desc)\n+\t),\n+\n+\tTP_fast_assign(\n+\t\tLOCAL_ASSIGN;\n+\t\tVIF_ASSIGN;\n+\t\tSTA_ASSIGN;\n+\t\t__entry-\u003en_desc = n_desc;\n+\t),\n+\n+\tTP_printk(\n+\t\tLOCAL_PR_FMT VIF_PR_FMT STA_PR_FMT \" %d descriptors\",\n+\t\tLOCAL_PR_ARG, VIF_PR_ARG, STA_PR_ARG, __entry-\u003en_desc\n+\t)\n+);\n+\n DEFINE_EVENT(sta_event, drv_sta_statistics,\n \tTP_PROTO(struct ieee80211_local *local,\n \t\t struct ieee80211_sub_if_data *sdata,\ndiff --git a/net/mac80211/util.c b/net/mac80211/util.c\nindex e2b5b2b21c347..c97a3e00e2518 100644\n--- a/net/mac80211/util.c\n+++ b/net/mac80211/util.c\n@@ -1744,6 +1744,8 @@ static void ieee80211_reconfig_stations(struct ieee80211_sub_if_data *sdata)\n \t\t state \u003c sta-\u003esta_state; state++)\n \t\t\tWARN_ON(drv_sta_state(local, sta-\u003esdata, sta, state,\n \t\t\t\t\t state + 1));\n+\n+\t\tieee80211_sta_scs_reconfig(sta);\n \t}\n }\n \ndiff --git a/net/mac80211/wme.c b/net/mac80211/wme.c\nindex 1601be5764145..9fcee1bc6ad59 100644\n--- a/net/mac80211/wme.c\n+++ b/net/mac80211/wme.c\n@@ -174,6 +174,13 @@ u16 ieee80211_select_queue(struct ieee80211_sub_if_data *sdata,\n \t\tgoto downgrade;\n \t}\n \n+\t/* A stream priority overrides the received one of a forwarded frame */\n+\tif (sta \u0026\u0026\n+\t (sdata-\u003evif.type == NL80211_IFTYPE_AP ||\n+\t sdata-\u003evif.type == NL80211_IFTYPE_AP_VLAN) \u0026\u0026\n+\t ieee80211_flow_classify(sta, skb))\n+\t\tgoto downgrade;\n+\n \t/* use the data classifier to determine what 802.1d tag the\n \t * data frame has */\n \tqos_map = rcu_dereference(sdata-\u003eqos_map);\ndiff --git a/net/wireless/Makefile b/net/wireless/Makefile\nindex a77fd5ba63686..552f4557ca23d 100644\n--- a/net/wireless/Makefile\n+++ b/net/wireless/Makefile\n@@ -8,7 +8,7 @@ obj-$(CONFIG_WEXT_PRIV) += wext-priv.o\n \n cfg80211-y += core.o sysfs.o radiotap.o util.o reg.o scan.o nl80211.o\n cfg80211-y += mlme.o ibss.o sme.o chan.o ethtool.o mesh.o ap.o trace.o ocb.o\n-cfg80211-y += michael-mic.o pmsr.o\n+cfg80211-y += michael-mic.o pmsr.o scs.o\n cfg80211-$(CONFIG_OF) += of.o\n cfg80211-$(CONFIG_CFG80211_DEBUGFS) += debugfs.o\n cfg80211-$(CONFIG_CFG80211_WEXT) += wext-compat.o wext-sme.o\ndiff --git a/net/wireless/core.c b/net/wireless/core.c\nindex cde3ca85494de..d7b47cbbf5427 100644\n--- a/net/wireless/core.c\n+++ b/net/wireless/core.c\n@@ -999,6 +999,16 @@ int wiphy_register(struct wiphy *wiphy)\n \t\t rdev-\u003eops-\u003eupdate_connect_params))\n \t\treturn -EINVAL;\n \n+\tif (WARN_ON(wiphy_ext_feature_isset(\u0026rdev-\u003ewiphy,\n+\t\t\t\t\t NL80211_EXT_FEATURE_SCS) \u0026\u0026\n+\t\t !rdev-\u003eops-\u003eset_scs))\n+\t\treturn -EINVAL;\n+\n+\tif (WARN_ON(wiphy_ext_feature_isset(\u0026rdev-\u003ewiphy,\n+\t\t\t\t\t NL80211_EXT_FEATURE_MSCS) \u0026\u0026\n+\t\t !rdev-\u003eops-\u003eset_mscs))\n+\t\treturn -EINVAL;\n+\n \tif (wiphy-\u003eaddresses)\n \t\tmemcpy(wiphy-\u003eperm_addr, wiphy-\u003eaddresses[0].addr, ETH_ALEN);\n \ndiff --git a/net/wireless/core.h b/net/wireless/core.h\nindex 6138d207caf41..9e8c2c42d67a7 100644\n--- a/net/wireless/core.h\n+++ b/net/wireless/core.h\n@@ -648,6 +648,14 @@ struct cfg80211_colocated_ap {\n \ts8 psd_20;\n };\n \n+int cfg80211_tclas_count(const u8 *elems, size_t len);\n+int cfg80211_parse_tclas(const u8 *elems, size_t len,\n+\t\t\t struct cfg80211_tclas *out, u8 n_tclas,\n+\t\t\t enum cfg80211_tclas_processing *processing);\n+int cfg80211_parse_tclas_mask(const u8 *elems, size_t len, u32 *fields);\n+bool cfg80211_scs_desc_valid(const struct cfg80211_scs_desc *desc);\n+bool cfg80211_mscs_desc_valid(const struct cfg80211_mscs_desc *desc);\n+\n #if IS_ENABLED(CONFIG_CFG80211_KUNIT_TEST)\n #define EXPORT_SYMBOL_IF_CFG80211_KUNIT(sym) EXPORT_SYMBOL_IF_KUNIT(sym)\n #define VISIBLE_IF_CFG80211_KUNIT\ndiff --git a/net/wireless/nl80211.c b/net/wireless/nl80211.c\nindex 85c010793c53e..b7a16a478ad04 100644\n--- a/net/wireless/nl80211.c\n+++ b/net/wireless/nl80211.c\n@@ -624,6 +624,31 @@ sar_policy[NL80211_SAR_ATTR_MAX + 1] = {\n \t[NL80211_SAR_ATTR_SPECS] = NLA_POLICY_NESTED_ARRAY(sar_specs_policy),\n };\n \n+/*\n+ * The element parsers validate the binary attributes, so that a malformed\n+ * element is answered with a status code instead of a netlink error.\n+ */\n+static const struct nla_policy\n+nl80211_scs_desc_policy[NL80211_SCS_DESC_ATTR_MAX + 1] = {\n+\t[NL80211_SCS_DESC_ATTR_ID] = NLA_POLICY_MIN(NLA_U8, 1),\n+\t[NL80211_SCS_DESC_ATTR_REQ_TYPE] =\n+\t\tNLA_POLICY_MAX(NLA_U8, NL80211_SCS_REQ_CHANGE),\n+\t[NL80211_SCS_DESC_ATTR_UP] = NLA_POLICY_MAX(NLA_U8, 7),\n+\t[NL80211_SCS_DESC_ATTR_TCLAS] = { .type = NLA_BINARY },\n+\t[NL80211_SCS_DESC_ATTR_QOS_CHAR] = { .type = NLA_BINARY },\n+\t[NL80211_SCS_DESC_ATTR_STATUS] = { .type = NLA_REJECT },\n+};\n+\n+static const struct nla_policy\n+nl80211_mscs_desc_policy[NL80211_MSCS_DESC_ATTR_MAX + 1] = {\n+\t[NL80211_MSCS_DESC_ATTR_REQ_TYPE] =\n+\t\tNLA_POLICY_MAX(NLA_U8, NL80211_SCS_REQ_CHANGE),\n+\t[NL80211_MSCS_DESC_ATTR_UP_BITMAP] = { .type = NLA_U8 },\n+\t[NL80211_MSCS_DESC_ATTR_UP_LIMIT] = NLA_POLICY_MAX(NLA_U8, 7),\n+\t[NL80211_MSCS_DESC_ATTR_STREAM_TIMEOUT] = { .type = NLA_U32 },\n+\t[NL80211_MSCS_DESC_ATTR_TCLAS_MASK] = { .type = NLA_BINARY },\n+};\n+\n static const struct nla_policy\n nl80211_mbssid_config_policy[NL80211_MBSSID_CONFIG_ATTR_MAX + 1] = {\n \t[NL80211_MBSSID_CONFIG_ATTR_MAX_INTERFACES] = NLA_POLICY_MIN(NLA_U8, 2),\n@@ -1098,6 +1123,10 @@ static const struct nla_policy nl80211_policy[NUM_NL80211_ATTR] = {\n \t\tNLA_POLICY_FULL_RANGE(NLA_U32, \u0026nl80211_punct_bitmap_range),\n \t[NL80211_ATTR_STA_DUMP_LINK_STATS] = { .type = NLA_FLAG },\n \t[NL80211_ATTR_FRAME_NO_STA] = { .type = NLA_FLAG },\n+\t[NL80211_ATTR_SCS_DESCRIPTORS] =\n+\t\tNLA_POLICY_NESTED_ARRAY(nl80211_scs_desc_policy),\n+\t[NL80211_ATTR_MSCS_DESCRIPTOR] =\n+\t\tNLA_POLICY_NESTED(nl80211_mscs_desc_policy),\n };\n \n /* policy for the key attributes */\n@@ -18288,6 +18317,390 @@ static int nl80211_set_qos_map(struct sk_buff *skb,\n \treturn ret;\n }\n \n+#define NL80211_MAX_SCS_DESC\t255\n+\n+/* A station can install the traffic description of its uplink stream */\n+static int nl80211_scs_iftype_ok(struct net_device *dev)\n+{\n+\tswitch (dev-\u003eieee80211_ptr-\u003eiftype) {\n+\tcase NL80211_IFTYPE_AP:\n+\tcase NL80211_IFTYPE_P2P_GO:\n+\tcase NL80211_IFTYPE_STATION:\n+\t\treturn 0;\n+\tdefault:\n+\t\treturn -EOPNOTSUPP;\n+\t}\n+}\n+\n+/* Only an AP mirrors an uplink priority onto the downlink */\n+static int nl80211_mscs_iftype_ok(struct net_device *dev)\n+{\n+\tswitch (dev-\u003eieee80211_ptr-\u003eiftype) {\n+\tcase NL80211_IFTYPE_AP:\n+\tcase NL80211_IFTYPE_P2P_GO:\n+\t\treturn 0;\n+\tdefault:\n+\t\treturn -EOPNOTSUPP;\n+\t}\n+}\n+\n+static u16 nl80211_scs_status(int err)\n+{\n+\tswitch (err) {\n+\tcase -EOPNOTSUPP:\n+\t\treturn WLAN_STATUS_REQUESTED_TCLAS_NOT_SUPPORTED_BY_AP;\n+\tcase -ENOSPC:\n+\t\treturn WLAN_STATUS_INSUFFICIENT_TCLAS_PROCESSING_RESOURCES;\n+\tdefault:\n+\t\treturn WLAN_STATUS_REQUEST_DECLINED;\n+\t}\n+}\n+\n+static void nl80211_free_scs_desc(struct cfg80211_scs_desc **desc,\n+\t\t\t\t unsigned int n)\n+{\n+\tunsigned int i;\n+\n+\tfor (i = 0; i \u003c n; i++)\n+\t\tkfree(desc[i]);\n+\n+\tkfree(desc);\n+}\n+\n+static struct cfg80211_scs_desc *\n+nl80211_parse_scs_desc(struct nlattr *attr, struct cfg80211_scs_result *res,\n+\t\t struct genl_info *info)\n+{\n+\tenum cfg80211_tclas_processing processing =\n+\t\tCFG80211_TCLAS_PROCESSING_ABSENT;\n+\tstruct nlattr *tb[NL80211_SCS_DESC_ATTR_MAX + 1];\n+\tconst struct ieee80211_qos_char_elem *qos_char = NULL;\n+\tstruct nlattr *qos_attr, *tclas_attr;\n+\tstruct cfg80211_scs_desc *desc;\n+\tbool wants_up = true;\n+\tint n_tclas = 0;\n+\tbool remove;\n+\tint err;\n+\n+\terr = nla_parse_nested(tb, NL80211_SCS_DESC_ATTR_MAX, attr,\n+\t\t\t nl80211_scs_desc_policy, info-\u003eextack);\n+\tif (err)\n+\t\treturn ERR_PTR(err);\n+\n+\tif (!tb[NL80211_SCS_DESC_ATTR_ID] ||\n+\t !tb[NL80211_SCS_DESC_ATTR_REQ_TYPE]) {\n+\t\tGENL_SET_ERR_MSG(info,\n+\t\t\t\t \"SCS descriptor without an ID or a request type\");\n+\t\treturn ERR_PTR(-EINVAL);\n+\t}\n+\n+\tremove = nla_get_u8(tb[NL80211_SCS_DESC_ATTR_REQ_TYPE]) ==\n+\t\t NL80211_SCS_REQ_REMOVE;\n+\n+\t/* A removal names only an SCSID, the elements are ignored */\n+\tqos_attr = remove ? NULL : tb[NL80211_SCS_DESC_ATTR_QOS_CHAR];\n+\tif (qos_attr \u0026\u0026 ieee80211_qos_char_size_ok(nla_data(qos_attr),\n+\t\t\t\t\t\t nla_len(qos_attr)))\n+\t\tqos_char = nla_data(qos_attr);\n+\n+\t/* An uplink or direct link descriptor carries no user priority */\n+\tif (qos_char \u0026\u0026 ieee80211_qos_char_direction(qos_char) !=\n+\t\t\tIEEE80211_QOS_CHAR_DIR_DOWNLINK)\n+\t\twants_up = false;\n+\n+\t/* Decline only this descriptor, the rest of the request stands */\n+\tif (!remove \u0026\u0026 (!qos_attr || qos_char) \u0026\u0026\n+\t wants_up != !!tb[NL80211_SCS_DESC_ATTR_UP])\n+\t\terr = -EINVAL;\n+\n+\ttclas_attr = remove ? NULL : tb[NL80211_SCS_DESC_ATTR_TCLAS];\n+\tif (!err \u0026\u0026 tclas_attr) {\n+\t\tn_tclas = cfg80211_tclas_count(nla_data(tclas_attr),\n+\t\t\t\t\t nla_len(tclas_attr));\n+\t\tif (n_tclas \u003c 0) {\n+\t\t\terr = n_tclas;\n+\t\t\tn_tclas = 0;\n+\t\t}\n+\t}\n+\n+\tdesc = kzalloc(struct_size(desc, tclas, n_tclas), GFP_KERNEL);\n+\tif (!desc)\n+\t\treturn ERR_PTR(-ENOMEM);\n+\n+\tdesc-\u003eid = nla_get_u8(tb[NL80211_SCS_DESC_ATTR_ID]);\n+\tdesc-\u003ereq_type = nla_get_u8(tb[NL80211_SCS_DESC_ATTR_REQ_TYPE]);\n+\tdesc-\u003eup = nla_get_u8_default(tb[NL80211_SCS_DESC_ATTR_UP], 0);\n+\tdesc-\u003en_tclas = n_tclas;\n+\n+\tif (!err \u0026\u0026 tclas_attr) {\n+\t\terr = cfg80211_parse_tclas(nla_data(tclas_attr),\n+\t\t\t\t\t nla_len(tclas_attr), desc-\u003etclas,\n+\t\t\t\t\t n_tclas, \u0026processing);\n+\t\tif (err) {\n+\t\t\tdesc-\u003en_tclas = 0;\n+\t\t\tprocessing = CFG80211_TCLAS_PROCESSING_ABSENT;\n+\t\t}\n+\t}\n+\n+\tdesc-\u003etclas_processing = processing;\n+\n+\tif (!err \u0026\u0026 qos_attr) {\n+\t\tif (!qos_char) {\n+\t\t\tNL_SET_ERR_MSG(info-\u003eextack,\n+\t\t\t\t \"bad QoS Characteristics element\");\n+\t\t\terr = -EINVAL;\n+\t\t} else {\n+\t\t\tdesc-\u003eqos_char = qos_char;\n+\t\t\tdesc-\u003eqos_char_len = nla_len(qos_attr);\n+\t\t}\n+\t}\n+\n+\tif (err)\n+\t\tres-\u003estatus = nl80211_scs_status(err);\n+\telse if (!cfg80211_scs_desc_valid(desc))\n+\t\tres-\u003estatus = WLAN_STATUS_REQUEST_DECLINED;\n+\n+\treturn desc;\n+}\n+\n+static int nl80211_set_scs_reply(struct genl_info *info,\n+\t\t\t\t struct cfg80211_scs_desc * const *desc,\n+\t\t\t\t const struct cfg80211_scs_result *res,\n+\t\t\t\t unsigned int n_desc)\n+{\n+\tstruct nlattr *list, *entry;\n+\tstruct sk_buff *msg;\n+\tunsigned int i;\n+\tsize_t size;\n+\tvoid *hdr;\n+\n+\tsize = nla_total_size(nla_total_size(sizeof(u8)) +\n+\t\t\t nla_total_size(sizeof(u16)));\n+\n+\tmsg = nlmsg_new(NLMSG_DEFAULT_SIZE + n_desc * size, GFP_KERNEL);\n+\tif (!msg)\n+\t\treturn -ENOMEM;\n+\n+\thdr = nl80211hdr_put(msg, info-\u003esnd_portid, info-\u003esnd_seq, 0,\n+\t\t\t NL80211_CMD_SET_SCS);\n+\tif (!hdr)\n+\t\tgoto nla_put_failure;\n+\n+\tlist = nla_nest_start(msg, NL80211_ATTR_SCS_DESCRIPTORS);\n+\tif (!list)\n+\t\tgoto nla_put_failure;\n+\n+\tfor (i = 0; i \u003c n_desc; i++) {\n+\t\tentry = nla_nest_start(msg, i + 1);\n+\t\tif (!entry)\n+\t\t\tgoto nla_put_failure;\n+\n+\t\tif (nla_put_u8(msg, NL80211_SCS_DESC_ATTR_ID, desc[i]-\u003eid) ||\n+\t\t nla_put_u16(msg, NL80211_SCS_DESC_ATTR_STATUS,\n+\t\t\t\tres[i].status))\n+\t\t\tgoto nla_put_failure;\n+\n+\t\tnla_nest_end(msg, entry);\n+\t}\n+\n+\tnla_nest_end(msg, list);\n+\tgenlmsg_end(msg, hdr);\n+\n+\treturn genlmsg_reply(msg, info);\n+\n+nla_put_failure:\n+\tnlmsg_free(msg);\n+\n+\treturn -ENOBUFS;\n+}\n+\n+static int nl80211_set_scs(struct sk_buff *skb, struct genl_info *info)\n+{\n+\tstruct cfg80211_registered_device *rdev = info-\u003euser_ptr[0];\n+\tstruct net_device *dev = info-\u003euser_ptr[1];\n+\tunsigned int i, n_desc = 0, n_apply = 0;\n+\tstruct cfg80211_scs_result *res;\n+\tstruct cfg80211_scs_desc **desc;\n+\tstruct nlattr *nla;\n+\tint rem, err;\n+\n+\tif (!wiphy_ext_feature_isset(\u0026rdev-\u003ewiphy, NL80211_EXT_FEATURE_SCS))\n+\t\treturn -EOPNOTSUPP;\n+\n+\tif (!info-\u003eattrs[NL80211_ATTR_MAC] ||\n+\t !info-\u003eattrs[NL80211_ATTR_SCS_DESCRIPTORS])\n+\t\treturn -EINVAL;\n+\n+\tif (info-\u003eattrs[NL80211_ATTR_MLO_LINK_ID]) {\n+\t\tGENL_SET_ERR_MSG(info,\n+\t\t\t\t \"SCS rules belong to the MLD, not to a link\");\n+\t\treturn -EINVAL;\n+\t}\n+\n+\terr = nl80211_scs_iftype_ok(dev);\n+\tif (err)\n+\t\treturn err;\n+\n+\tnla_for_each_nested(nla, info-\u003eattrs[NL80211_ATTR_SCS_DESCRIPTORS], rem)\n+\t\tn_desc++;\n+\n+\tif (!n_desc || n_desc \u003e NL80211_MAX_SCS_DESC)\n+\t\treturn -EINVAL;\n+\n+\tdesc = kcalloc(n_desc, sizeof(*desc), GFP_KERNEL);\n+\tif (!desc)\n+\t\treturn -ENOMEM;\n+\n+\tres = kcalloc(n_desc, sizeof(*res), GFP_KERNEL);\n+\tif (!res) {\n+\t\tkfree(desc);\n+\t\treturn -ENOMEM;\n+\t}\n+\n+\ti = 0;\n+\tnla_for_each_nested(nla, info-\u003eattrs[NL80211_ATTR_SCS_DESCRIPTORS],\n+\t\t\t rem) {\n+\t\tdesc[i] = nl80211_parse_scs_desc(nla, \u0026res[i], info);\n+\t\tif (IS_ERR(desc[i])) {\n+\t\t\terr = PTR_ERR(desc[i]);\n+\t\t\tdesc[i] = NULL;\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\ti++;\n+\t}\n+\n+\t/* Pass only the descriptors that were not declined to the driver */\n+\tfor (i = 0; i \u003c n_desc; i++) {\n+\t\tif (res[i].status != WLAN_STATUS_SUCCESS)\n+\t\t\tcontinue;\n+\n+\t\tswap(desc[i], desc[n_apply]);\n+\t\tswap(res[i], res[n_apply]);\n+\t\tn_apply++;\n+\t}\n+\n+\tif (n_apply) {\n+\t\terr = rdev_set_scs(rdev, dev,\n+\t\t\t\t nla_data(info-\u003eattrs[NL80211_ATTR_MAC]),\n+\t\t\t\t desc, res, n_apply);\n+\t\tif (err)\n+\t\t\tgoto out;\n+\t}\n+\n+\terr = nl80211_set_scs_reply(info, desc, res, n_desc);\n+\n+out:\n+\tkfree(res);\n+\tnl80211_free_scs_desc(desc, n_desc);\n+\n+\treturn err;\n+}\n+\n+static int nl80211_send_status_code(struct genl_info *info, u32 cmd, u16 status)\n+{\n+\tstruct sk_buff *msg;\n+\tvoid *hdr;\n+\n+\tmsg = nlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL);\n+\tif (!msg)\n+\t\treturn -ENOMEM;\n+\n+\thdr = nl80211hdr_put(msg, info-\u003esnd_portid, info-\u003esnd_seq, 0, cmd);\n+\tif (!hdr || nla_put_u16(msg, NL80211_ATTR_STATUS_CODE, status)) {\n+\t\tnlmsg_free(msg);\n+\t\treturn -ENOBUFS;\n+\t}\n+\n+\tgenlmsg_end(msg, hdr);\n+\n+\treturn genlmsg_reply(msg, info);\n+}\n+\n+static int nl80211_set_mscs(struct sk_buff *skb, struct genl_info *info)\n+{\n+\tstruct cfg80211_registered_device *rdev = info-\u003euser_ptr[0];\n+\tstruct nlattr *tb[NL80211_MSCS_DESC_ATTR_MAX + 1];\n+\tstruct net_device *dev = info-\u003euser_ptr[1];\n+\tstruct cfg80211_mscs_desc desc = {};\n+\tu16 status = WLAN_STATUS_SUCCESS;\n+\tstruct nlattr *mask;\n+\tbool remove;\n+\tint err;\n+\n+\tif (!wiphy_ext_feature_isset(\u0026rdev-\u003ewiphy, NL80211_EXT_FEATURE_MSCS))\n+\t\treturn -EOPNOTSUPP;\n+\n+\tif (!info-\u003eattrs[NL80211_ATTR_MAC] ||\n+\t !info-\u003eattrs[NL80211_ATTR_MSCS_DESCRIPTOR])\n+\t\treturn -EINVAL;\n+\n+\tif (info-\u003eattrs[NL80211_ATTR_MLO_LINK_ID]) {\n+\t\tGENL_SET_ERR_MSG(info,\n+\t\t\t\t \"an MSCS belongs to the MLD, not to a link\");\n+\t\treturn -EINVAL;\n+\t}\n+\n+\terr = nl80211_mscs_iftype_ok(dev);\n+\tif (err)\n+\t\treturn err;\n+\n+\terr = nla_parse_nested(tb, NL80211_MSCS_DESC_ATTR_MAX,\n+\t\t\t info-\u003eattrs[NL80211_ATTR_MSCS_DESCRIPTOR],\n+\t\t\t nl80211_mscs_desc_policy, info-\u003eextack);\n+\tif (err)\n+\t\treturn err;\n+\n+\tif (!tb[NL80211_MSCS_DESC_ATTR_REQ_TYPE]) {\n+\t\tGENL_SET_ERR_MSG(info,\n+\t\t\t\t \"MSCS descriptor without a request type\");\n+\t\treturn -EINVAL;\n+\t}\n+\n+\tdesc.req_type = nla_get_u8(tb[NL80211_MSCS_DESC_ATTR_REQ_TYPE]);\n+\tremove = desc.req_type == NL80211_SCS_REQ_REMOVE;\n+\n+\tif (remove == !!tb[NL80211_MSCS_DESC_ATTR_TCLAS_MASK]) {\n+\t\tGENL_SET_ERR_MSG(info,\n+\t\t\t\t \"TCLAS Mask does not match the request type\");\n+\t\treturn -EINVAL;\n+\t}\n+\n+\tdesc.up_bitmap =\n+\t\tnla_get_u8_default(tb[NL80211_MSCS_DESC_ATTR_UP_BITMAP], 0);\n+\tdesc.up_limit = nla_get_u8_default(tb[NL80211_MSCS_DESC_ATTR_UP_LIMIT],\n+\t\t\t\t\t 7);\n+\tdesc.stream_timeout =\n+\t\tnla_get_u32_default(tb[NL80211_MSCS_DESC_ATTR_STREAM_TIMEOUT],\n+\t\t\t\t 0);\n+\n+\tmask = tb[NL80211_MSCS_DESC_ATTR_TCLAS_MASK];\n+\tif (mask) {\n+\t\terr = cfg80211_parse_tclas_mask(nla_data(mask), nla_len(mask),\n+\t\t\t\t\t\t\u0026desc.fields);\n+\t\tif (err) {\n+\t\t\tstatus = nl80211_scs_status(err);\n+\t\t\tgoto reply;\n+\t\t}\n+\t}\n+\n+\tif (!cfg80211_mscs_desc_valid(\u0026desc)) {\n+\t\tstatus = WLAN_STATUS_REQUEST_DECLINED;\n+\t\tgoto reply;\n+\t}\n+\n+\terr = rdev_set_mscs(rdev, dev,\n+\t\t\t nla_data(info-\u003eattrs[NL80211_ATTR_MAC]), \u0026desc);\n+\tif (err)\n+\t\treturn err;\n+\n+\tif (remove)\n+\t\tstatus = WLAN_STATUS_TCLAS_PROCESSING_TERMINATED;\n+\n+reply:\n+\treturn nl80211_send_status_code(info, NL80211_CMD_SET_MSCS, status);\n+}\n+\n static int nl80211_add_tx_ts(struct sk_buff *skb, struct genl_info *info)\n {\n \tstruct cfg80211_registered_device *rdev = info-\u003euser_ptr[0];\n@@ -20446,6 +20859,18 @@ static const struct genl_small_ops nl80211_small_ops[] = {\n \t\t.flags = GENL_UNS_ADMIN_PERM,\n \t\t.internal_flags = IFLAGS(NL80211_FLAG_NEED_NETDEV_UP),\n \t},\n+\t{\n+\t\t.cmd = NL80211_CMD_SET_SCS,\n+\t\t.doit = nl80211_set_scs,\n+\t\t.flags = GENL_UNS_ADMIN_PERM,\n+\t\t.internal_flags = IFLAGS(NL80211_FLAG_NEED_NETDEV_UP),\n+\t},\n+\t{\n+\t\t.cmd = NL80211_CMD_SET_MSCS,\n+\t\t.doit = nl80211_set_mscs,\n+\t\t.flags = GENL_UNS_ADMIN_PERM,\n+\t\t.internal_flags = IFLAGS(NL80211_FLAG_NEED_NETDEV_UP),\n+\t},\n \t{\n \t\t.cmd = NL80211_CMD_ADD_TX_TS,\n \t\t.validate = GENL_DONT_VALIDATE_STRICT | GENL_DONT_VALIDATE_DUMP,\ndiff --git a/net/wireless/rdev-ops.h b/net/wireless/rdev-ops.h\nindex adcfd0278da32..8d5c99be74137 100644\n--- a/net/wireless/rdev-ops.h\n+++ b/net/wireless/rdev-ops.h\n@@ -1188,6 +1188,38 @@ static inline int rdev_set_qos_map(struct cfg80211_registered_device *rdev,\n \treturn ret;\n }\n \n+static inline int rdev_set_scs(struct cfg80211_registered_device *rdev,\n+\t\t\t struct net_device *dev, const u8 *peer,\n+\t\t\t struct cfg80211_scs_desc * const *desc,\n+\t\t\t struct cfg80211_scs_result *res, u8 n_desc)\n+{\n+\tint ret = -EOPNOTSUPP;\n+\n+\tif (rdev-\u003eops-\u003eset_scs) {\n+\t\ttrace_rdev_set_scs(\u0026rdev-\u003ewiphy, dev, peer, n_desc);\n+\t\tret = rdev-\u003eops-\u003eset_scs(\u0026rdev-\u003ewiphy, dev, peer, desc, res,\n+\t\t\t\t\t n_desc);\n+\t\ttrace_rdev_return_int(\u0026rdev-\u003ewiphy, ret);\n+\t}\n+\n+\treturn ret;\n+}\n+\n+static inline int rdev_set_mscs(struct cfg80211_registered_device *rdev,\n+\t\t\t\tstruct net_device *dev, const u8 *peer,\n+\t\t\t\tstruct cfg80211_mscs_desc *desc)\n+{\n+\tint ret = -EOPNOTSUPP;\n+\n+\tif (rdev-\u003eops-\u003eset_mscs) {\n+\t\ttrace_rdev_set_mscs(\u0026rdev-\u003ewiphy, dev, peer, desc);\n+\t\tret = rdev-\u003eops-\u003eset_mscs(\u0026rdev-\u003ewiphy, dev, peer, desc);\n+\t\ttrace_rdev_return_int(\u0026rdev-\u003ewiphy, ret);\n+\t}\n+\n+\treturn ret;\n+}\n+\n static inline int\n rdev_set_ap_chanwidth(struct cfg80211_registered_device *rdev,\n \t\t struct net_device *dev,\ndiff --git a/net/wireless/scs.c b/net/wireless/scs.c\nnew file mode 100644\nindex 0000000000000..22c0b9d6930e2\n--- /dev/null\n+++ b/net/wireless/scs.c\n@@ -0,0 +1,982 @@\n+// SPDX-License-Identifier: GPL-2.0-only\n+/*\n+ * Stream classification service (SCS) and mirrored SCS (MSCS)\n+ *\n+ * Copyright (C) 2026 Felix Fietkau \u003cnbd@nbd.name\u003e\n+ */\n+#include \u003ckunit/visibility.h\u003e\n+#include \u003clinux/ieee80211.h\u003e\n+#include \u003clinux/if_vlan.h\u003e\n+#include \u003clinux/ip.h\u003e\n+#include \u003clinux/ipv6.h\u003e\n+#include \u003clinux/unaligned.h\u003e\n+#include \u003cnet/cfg80211.h\u003e\n+#include \u003cnet/dsfield.h\u003e\n+#include \u003cnet/ip.h\u003e\n+#include \u003cnet/ipv6.h\u003e\n+#include \"core.h\"\n+\n+enum cfg80211_tclas_type {\n+\tCFG80211_TCLAS_ETH\t\t= 0,\n+\tCFG80211_TCLAS_IP\t\t= 4,\n+\tCFG80211_TCLAS_VLAN\t\t= 5,\n+\tCFG80211_TCLAS_FILTER\t\t= 10,\n+};\n+\n+struct tclas_fc {\n+\tu8 type;\n+\tu8 mask;\n+} __packed;\n+\n+struct tclas_fc_eth {\n+\tstruct tclas_fc hdr;\n+\tu8 sa[ETH_ALEN];\n+\tu8 da[ETH_ALEN];\n+\t__be16 ethertype;\n+} __packed;\n+\n+struct tclas_fc_ip4 {\n+\tstruct tclas_fc hdr;\n+\tu8 version;\n+\t__be32 src;\n+\t__be32 dst;\n+\t__be16 src_port;\n+\t__be16 dst_port;\n+\tu8 dscp;\n+\tu8 proto;\n+\tu8 reserved;\n+} __packed;\n+\n+struct tclas_fc_ip6 {\n+\tstruct tclas_fc hdr;\n+\tu8 version;\n+\tstruct in6_addr src;\n+\tstruct in6_addr dst;\n+\t__be16 src_port;\n+\t__be16 dst_port;\n+\tu8 dscp;\n+\tu8 proto;\n+\tu8 flow_label[3];\n+} __packed;\n+\n+struct tclas_fc_vlan {\n+\tstruct tclas_fc hdr;\n+\tu8 pcp;\n+\tu8 dei;\n+\t__be16 vid;\n+} __packed;\n+\n+/* Hashed and compared as a byte string, so it must contain no padding */\n+static_assert(sizeof(struct cfg80211_flow_key) ==\n+\t 2 * sizeof(struct in6_addr) + sizeof(__be32) + 2 * ETH_ALEN +\n+\t 4 * sizeof(__be16) + 4);\n+\n+#define FLOW_F(name)\tBIT(CFG80211_FLOW_F_##name)\n+\n+#define FLOW_F_VLAN\t(FLOW_F(VLAN_PCP) | FLOW_F(VLAN_DEI) | FLOW_F(VLAN_VID))\n+\n+/* Reserved classifier mask bits above the array length are ignored */\n+static const u8 tclas_map_eth[] = {\n+\tCFG80211_FLOW_F_ETH_SA,\n+\tCFG80211_FLOW_F_ETH_DA,\n+\tCFG80211_FLOW_F_ETH_TYPE,\n+};\n+\n+/* Shared by both IP versions, only IPv6 has the flow label */\n+static const u8 tclas_map_ip[] = {\n+\tCFG80211_FLOW_F_IP_VERSION,\n+\tCFG80211_FLOW_F_IP_SRC,\n+\tCFG80211_FLOW_F_IP_DST,\n+\tCFG80211_FLOW_F_SRC_PORT,\n+\tCFG80211_FLOW_F_DST_PORT,\n+\tCFG80211_FLOW_F_DSCP,\n+\tCFG80211_FLOW_F_PROTO,\n+\tCFG80211_FLOW_F_FLOW_LABEL,\n+};\n+\n+static const u8 tclas_map_vlan[] = {\n+\tCFG80211_FLOW_F_VLAN_PCP,\n+\tCFG80211_FLOW_F_VLAN_DEI,\n+\tCFG80211_FLOW_F_VLAN_VID,\n+};\n+\n+static u32 tclas_mask_fields(u8 type, u8 version, u8 mask)\n+{\n+\tsize_t n = ARRAY_SIZE(tclas_map_ip);\n+\tconst u8 *map = tclas_map_ip;\n+\tunsigned int i;\n+\tu32 fields = 0;\n+\n+\tswitch (type) {\n+\tcase CFG80211_TCLAS_ETH:\n+\t\tmap = tclas_map_eth;\n+\t\tn = ARRAY_SIZE(tclas_map_eth);\n+\t\tbreak;\n+\tcase CFG80211_TCLAS_VLAN:\n+\t\tmap = tclas_map_vlan;\n+\t\tn = ARRAY_SIZE(tclas_map_vlan);\n+\t\tbreak;\n+\tdefault:\n+\t\tn -= version == 4;\n+\t\tbreak;\n+\t}\n+\n+\tfor (i = 0; i \u003c n; i++)\n+\t\tif (mask \u0026 BIT(i))\n+\t\t\tfields |= BIT(map[i]);\n+\n+\treturn fields;\n+}\n+\n+static void flow_key_select(const struct cfg80211_flow_key *src, u32 fields,\n+\t\t\t struct cfg80211_flow_key *dst)\n+{\n+\tmemset(dst, 0, sizeof(*dst));\n+\n+\tif (fields \u0026 FLOW_F(ETH_SA))\n+\t\tmemcpy(dst-\u003esa, src-\u003esa, ETH_ALEN);\n+\tif (fields \u0026 FLOW_F(ETH_DA))\n+\t\tmemcpy(dst-\u003eda, src-\u003eda, ETH_ALEN);\n+\tif (fields \u0026 FLOW_F(ETH_TYPE))\n+\t\tdst-\u003eeth_type = src-\u003eeth_type;\n+\n+\tif (fields \u0026 FLOW_F_VLAN) {\n+\t\tu16 tci = 0;\n+\n+\t\tif (fields \u0026 FLOW_F(VLAN_PCP))\n+\t\t\ttci |= VLAN_PRIO_MASK;\n+\t\tif (fields \u0026 FLOW_F(VLAN_DEI))\n+\t\t\ttci |= VLAN_CFI_MASK;\n+\t\tif (fields \u0026 FLOW_F(VLAN_VID))\n+\t\t\ttci |= VLAN_VID_MASK;\n+\n+\t\tdst-\u003evlan_tci = src-\u003evlan_tci \u0026 htons(tci);\n+\t}\n+\n+\tif (fields \u0026 FLOW_F(IP_VERSION))\n+\t\tdst-\u003eip_version = src-\u003eip_version;\n+\tif (fields \u0026 FLOW_F(IP_SRC))\n+\t\tdst-\u003esrc = src-\u003esrc;\n+\tif (fields \u0026 FLOW_F(IP_DST))\n+\t\tdst-\u003edst = src-\u003edst;\n+\tif (fields \u0026 FLOW_F(SRC_PORT))\n+\t\tdst-\u003esrc_port = src-\u003esrc_port;\n+\tif (fields \u0026 FLOW_F(DST_PORT))\n+\t\tdst-\u003edst_port = src-\u003edst_port;\n+\n+\tif (fields \u0026 FLOW_F(DSCP))\n+\t\tdst-\u003edscp = src-\u003edscp;\n+\tif (fields \u0026 FLOW_F(PROTO))\n+\t\tdst-\u003eproto = src-\u003eproto;\n+\tif (fields \u0026 FLOW_F(FLOW_LABEL))\n+\t\tdst-\u003eflow_label = src-\u003eflow_label;\n+}\n+\n+static int tclas_parse_eth(struct cfg80211_flow_key *key, const u8 *data,\n+\t\t\t size_t len)\n+{\n+\tconst struct tclas_fc_eth *fc = (const void *)data;\n+\n+\tif (len != sizeof(*fc))\n+\t\treturn -EINVAL;\n+\n+\tmemcpy(key-\u003esa, fc-\u003esa, ETH_ALEN);\n+\tmemcpy(key-\u003eda, fc-\u003eda, ETH_ALEN);\n+\tkey-\u003eeth_type = fc-\u003eethertype;\n+\n+\treturn 0;\n+}\n+\n+static int tclas_parse_ip4(struct cfg80211_flow_key *key,\n+\t\t\t const struct tclas_fc_ip4 *fc, bool values)\n+{\n+\tif (values \u0026\u0026 fc-\u003eversion != 4)\n+\t\treturn -EINVAL;\n+\n+\tkey-\u003eip_version = 4;\n+\tmemcpy(\u0026key-\u003esrc, \u0026fc-\u003esrc, sizeof(fc-\u003esrc));\n+\tmemcpy(\u0026key-\u003edst, \u0026fc-\u003edst, sizeof(fc-\u003edst));\n+\tkey-\u003esrc_port = fc-\u003esrc_port;\n+\tkey-\u003edst_port = fc-\u003edst_port;\n+\tkey-\u003edscp = fc-\u003edscp \u0026 0x3f;\n+\tkey-\u003eproto = fc-\u003eproto;\n+\n+\treturn 0;\n+}\n+\n+static int tclas_parse_ip6(struct cfg80211_flow_key *key,\n+\t\t\t const struct tclas_fc_ip6 *fc, bool values)\n+{\n+\tif (values \u0026\u0026 fc-\u003eversion != 6)\n+\t\treturn -EINVAL;\n+\n+\tkey-\u003eip_version = 6;\n+\tmemcpy(\u0026key-\u003esrc, \u0026fc-\u003esrc, sizeof(fc-\u003esrc));\n+\tmemcpy(\u0026key-\u003edst, \u0026fc-\u003edst, sizeof(fc-\u003edst));\n+\tkey-\u003esrc_port = fc-\u003esrc_port;\n+\tkey-\u003edst_port = fc-\u003edst_port;\n+\tkey-\u003edscp = fc-\u003edscp \u0026 0x3f;\n+\tkey-\u003eproto = fc-\u003eproto;\n+\t/* Same encoding as ip6_flowlabel(), so the two compare equal */\n+\tkey-\u003eflow_label =\n+\t\tcpu_to_be32(get_unaligned_be24(fc-\u003eflow_label) \u0026 0xfffff);\n+\n+\treturn 0;\n+}\n+\n+/* The Version subfield is reserved in a mask, so the length selects v4/v6 */\n+static int tclas_parse_ip(struct cfg80211_flow_key *key, const u8 *data,\n+\t\t\t size_t len, bool values)\n+{\n+\tif (len == sizeof(struct tclas_fc_ip4))\n+\t\treturn tclas_parse_ip4(key, (const void *)data, values);\n+\n+\tif (len == sizeof(struct tclas_fc_ip6))\n+\t\treturn tclas_parse_ip6(key, (const void *)data, values);\n+\n+\treturn -EINVAL;\n+}\n+\n+static int tclas_parse_vlan(struct cfg80211_flow_key *key, const u8 *data,\n+\t\t\t size_t len)\n+{\n+\tconst struct tclas_fc_vlan *fc = (const void *)data;\n+\tu16 tci;\n+\n+\tif (len != sizeof(*fc))\n+\t\treturn -EINVAL;\n+\n+\tif (fc-\u003epcp \u003e 7 || fc-\u003edei \u003e 1 || be16_to_cpu(fc-\u003evid) \u003e 0xfff)\n+\t\treturn -EINVAL;\n+\n+\ttci = fc-\u003epcp \u003c\u003c 13;\n+\ttci |= fc-\u003edei \u003c\u003c 12;\n+\ttci |= be16_to_cpu(fc-\u003evid);\n+\tkey-\u003evlan_tci = cpu_to_be16(tci);\n+\n+\treturn 0;\n+}\n+\n+/* The octets after the header are the value and the mask, of equal length */\n+static int tclas_parse_filter(struct cfg80211_tclas *t, const u8 *data,\n+\t\t\t size_t len)\n+{\n+\tstruct cfg80211_tclas_filter *f = \u0026t-\u003efilter;\n+\n+\tif (len \u003c 3 || (len - 3) % 2)\n+\t\treturn -EINVAL;\n+\n+\tf-\u003elen = (len - 3) / 2;\n+\tif (!f-\u003elen || f-\u003elen \u003e CFG80211_TCLAS_FILTER_LEN)\n+\t\treturn -EOPNOTSUPP;\n+\n+\tf-\u003einstance = data[1];\n+\tf-\u003eprotocol = data[2];\n+\tmemcpy(f-\u003evalue, data + 3, f-\u003elen);\n+\tmemcpy(f-\u003emask, data + 3 + f-\u003elen, f-\u003elen);\n+\n+\treturn 0;\n+}\n+\n+/* @values is false for a TCLAS Mask element, which carries no values */\n+static int cfg80211_parse_frame_classifier(struct cfg80211_tclas *t,\n+\t\t\t\t\t const u8 *data, size_t len,\n+\t\t\t\t\t bool values)\n+{\n+\tconst struct tclas_fc *fc = (const void *)data;\n+\tstruct cfg80211_flow_key raw = {};\n+\tint ret;\n+\n+\tif (len \u003c sizeof(*fc))\n+\t\treturn -EINVAL;\n+\n+\tmemset(t, 0, sizeof(*t));\n+\tt-\u003etype = fc-\u003etype;\n+\n+\tswitch (t-\u003etype) {\n+\tcase CFG80211_TCLAS_ETH:\n+\t\tret = tclas_parse_eth(\u0026raw, data, len);\n+\t\tbreak;\n+\tcase CFG80211_TCLAS_IP:\n+\t\tret = tclas_parse_ip(\u0026raw, data, len, values);\n+\t\tbreak;\n+\tcase CFG80211_TCLAS_VLAN:\n+\t\tret = tclas_parse_vlan(\u0026raw, data, len);\n+\t\tbreak;\n+\tcase CFG80211_TCLAS_FILTER:\n+\t\t/* A mask element selects fields, which a filter does not */\n+\t\tif (!values)\n+\t\t\treturn -EOPNOTSUPP;\n+\n+\t\treturn tclas_parse_filter(t, data, len);\n+\tdefault:\n+\t\treturn -EOPNOTSUPP;\n+\t}\n+\n+\tif (ret)\n+\t\treturn ret;\n+\n+\tt-\u003efields = tclas_mask_fields(t-\u003etype, raw.ip_version, fc-\u003emask);\n+\n+\t/* A mask that selects nothing would match every MSDU */\n+\tif (!t-\u003efields)\n+\t\treturn -EINVAL;\n+\n+\t/* Always match on the IP version, which the element length selects */\n+\tif (t-\u003etype == CFG80211_TCLAS_IP)\n+\t\tt-\u003efields |= FLOW_F(IP_VERSION);\n+\n+\tflow_key_select(\u0026raw, t-\u003efields, \u0026t-\u003ekey);\n+\n+\treturn 0;\n+}\n+\n+/**\n+ * cfg80211_tclas_count - count the TCLAS elements of a chain\n+ *\n+ * @elems: element chain, as it arrived over the air\n+ * @len: length of @elems\n+ *\n+ * Sizes the array for cfg80211_parse_tclas(). Does not validate the\n+ * elements.\n+ *\n+ * Return: the number of TCLAS elements, or -ENOSPC for more than a descriptor\n+ *\tcan hold.\n+ */\n+int cfg80211_tclas_count(const u8 *elems, size_t len)\n+{\n+\tconst struct element *elem;\n+\tunsigned int n = 0;\n+\n+\tfor_each_element_id(elem, WLAN_EID_TCLAS, elems, len)\n+\t\tn++;\n+\n+\tif (n \u003e U8_MAX)\n+\t\treturn -ENOSPC;\n+\n+\treturn n;\n+}\n+EXPORT_SYMBOL_IF_CFG80211_KUNIT(cfg80211_tclas_count);\n+\n+/**\n+ * cfg80211_parse_tclas - parse a chain of TCLAS elements\n+ *\n+ * @elems: element chain, as it arrived over the air\n+ * @len: length of @elems\n+ * @out: array that receives the parsed elements\n+ * @n_tclas: number of entries in @out, from cfg80211_tclas_count()\n+ * @processing: receives how the elements relate, or\n+ *\t%CFG80211_TCLAS_PROCESSING_ABSENT when the chain carries no TCLAS\n+ *\tProcessing element\n+ *\n+ * The chain holds TCLAS elements and at most one TCLAS Processing element.\n+ *\n+ * Return: 0 on success, -EINVAL for a malformed chain, -EOPNOTSUPP for a\n+ *\tclassifier the kernel cannot evaluate, -ENOSPC for a chain that no\n+ *\tlonger fits @out.\n+ */\n+int cfg80211_parse_tclas(const u8 *elems, size_t len,\n+\t\t\t struct cfg80211_tclas *out, u8 n_tclas,\n+\t\t\t enum cfg80211_tclas_processing *processing)\n+{\n+\tconst struct element *elem;\n+\tunsigned int n = 0;\n+\tint ret;\n+\n+\t*processing = CFG80211_TCLAS_PROCESSING_ABSENT;\n+\n+\tfor_each_element(elem, elems, len) {\n+\t\tswitch (elem-\u003eid) {\n+\t\tcase WLAN_EID_TCLAS:\n+\t\t\tif (n == n_tclas)\n+\t\t\t\treturn -ENOSPC;\n+\n+\t\t\tif (elem-\u003edatalen \u003c 1)\n+\t\t\t\treturn -EINVAL;\n+\n+\t\t\t/*\n+\t\t\t * Any other value matches on the UP of the MSDU,\n+\t\t\t * which is not known yet at classification time.\n+\t\t\t */\n+\t\t\tif (elem-\u003edata[0] != 255)\n+\t\t\t\treturn -EOPNOTSUPP;\n+\n+\t\t\tret = cfg80211_parse_frame_classifier(\u0026out[n],\n+\t\t\t\t\t\t\t elem-\u003edata + 1,\n+\t\t\t\t\t\t\t elem-\u003edatalen - 1,\n+\t\t\t\t\t\t\t true);\n+\t\t\tif (ret)\n+\t\t\t\treturn ret;\n+\n+\t\t\tn++;\n+\t\t\tbreak;\n+\t\tcase WLAN_EID_TCLAS_PROCESSING:\n+\t\t\tif (elem-\u003edatalen != 1 ||\n+\t\t\t *processing != CFG80211_TCLAS_PROCESSING_ABSENT ||\n+\t\t\t elem-\u003edata[0] \u003e CFG80211_TCLAS_PROCESSING_DEFAULT)\n+\t\t\t\treturn -EINVAL;\n+\n+\t\t\t*processing = elem-\u003edata[0];\n+\t\t\tbreak;\n+\t\tdefault:\n+\t\t\treturn -EINVAL;\n+\t\t}\n+\t}\n+\n+\tif (!for_each_element_completed(elem, elems, len))\n+\t\treturn -EINVAL;\n+\n+\treturn 0;\n+}\n+EXPORT_SYMBOL_IF_CFG80211_KUNIT(cfg80211_parse_tclas);\n+\n+/**\n+ * cfg80211_parse_tclas_mask - parse a chain of TCLAS Mask elements\n+ *\n+ * @elems: element chain, as it arrived over the air\n+ * @len: length of @elems\n+ * @fields: receives the union of the selected classifier parameters, as a\n+ *\tbitmap of \u0026enum cfg80211_flow_field\n+ *\n+ * A TCLAS Mask element carries no values, only the field selection.\n+ *\n+ * Return: 0 on success, -EINVAL for a malformed chain, -EOPNOTSUPP for a\n+ *\tclassifier type the kernel cannot evaluate.\n+ */\n+int cfg80211_parse_tclas_mask(const u8 *elems, size_t len, u32 *fields)\n+{\n+\tconst struct element *elem;\n+\tstruct cfg80211_tclas t;\n+\tint ret;\n+\n+\t*fields = 0;\n+\n+\tfor_each_element(elem, elems, len) {\n+\t\tif (elem-\u003eid != WLAN_EID_EXTENSION || elem-\u003edatalen \u003c 1 ||\n+\t\t elem-\u003edata[0] != WLAN_EID_EXT_TCLAS_MASK)\n+\t\t\treturn -EINVAL;\n+\n+\t\tret = cfg80211_parse_frame_classifier(\u0026t, elem-\u003edata + 1,\n+\t\t\t\t\t\t elem-\u003edatalen - 1, false);\n+\t\tif (ret)\n+\t\t\treturn ret;\n+\n+\t\t*fields |= t.fields;\n+\t}\n+\n+\tif (!for_each_element_completed(elem, elems, len))\n+\t\treturn -EINVAL;\n+\n+\treturn 0;\n+}\n+EXPORT_SYMBOL_IF_CFG80211_KUNIT(cfg80211_parse_tclas_mask);\n+\n+#define FLOW_F_PORTS\t(FLOW_F(SRC_PORT) | FLOW_F(DST_PORT))\n+\n+static void cfg80211_flow_parse_ports(struct sk_buff *skb,\n+\t\t\t\t unsigned int offset, int proto,\n+\t\t\t\t struct cfg80211_flow_info *info)\n+{\n+\t__be16 ports[2], *p;\n+\n+\tswitch (proto) {\n+\tcase IPPROTO_TCP:\n+\tcase IPPROTO_UDP:\n+\tcase IPPROTO_SCTP:\n+\t\tbreak;\n+\tdefault:\n+\t\treturn;\n+\t}\n+\n+\tp = skb_header_pointer(skb, offset, sizeof(ports), ports);\n+\tif (!p)\n+\t\treturn;\n+\n+\tinfo-\u003ekey.src_port = p[0];\n+\tinfo-\u003ekey.dst_port = p[1];\n+\tinfo-\u003epresent |= FLOW_F_PORTS;\n+}\n+\n+static void cfg80211_flow_parse_ipv4(struct sk_buff *skb, unsigned int offset,\n+\t\t\t\t struct cfg80211_flow_info *info)\n+{\n+\tstruct iphdr iph_buf, *iph;\n+\n+\tiph = skb_header_pointer(skb, offset, sizeof(*iph), \u0026iph_buf);\n+\tif (!iph || iph-\u003eversion != 4 || iph-\u003eihl \u003c 5)\n+\t\treturn;\n+\n+\tinfo-\u003ekey.ip_version = 4;\n+\tinfo-\u003ekey.dscp = ipv4_get_dsfield(iph) \u003e\u003e 2;\n+\tinfo-\u003ekey.proto = iph-\u003eprotocol;\n+\tmemcpy(\u0026info-\u003ekey.src, \u0026iph-\u003esaddr, sizeof(iph-\u003esaddr));\n+\tmemcpy(\u0026info-\u003ekey.dst, \u0026iph-\u003edaddr, sizeof(iph-\u003edaddr));\n+\tinfo-\u003epresent |= FLOW_F(IP_VERSION) | FLOW_F(IP_SRC) | FLOW_F(IP_DST) |\n+\t\t\t FLOW_F(DSCP) | FLOW_F(PROTO);\n+\n+\tif (iph-\u003efrag_off \u0026 htons(IP_OFFSET))\n+\t\treturn;\n+\n+\tcfg80211_flow_parse_ports(skb, offset + iph-\u003eihl * 4, iph-\u003eprotocol,\n+\t\t\t\t info);\n+}\n+\n+static void cfg80211_flow_parse_ipv6(struct sk_buff *skb, unsigned int offset,\n+\t\t\t\t struct cfg80211_flow_info *info)\n+{\n+\tstruct ipv6hdr ip6h_buf, *ip6h;\n+\t__be16 frag_off = 0;\n+\tint l4_off;\n+\tu8 nexthdr;\n+\n+\tip6h = skb_header_pointer(skb, offset, sizeof(*ip6h), \u0026ip6h_buf);\n+\tif (!ip6h || ip6h-\u003eversion != 6)\n+\t\treturn;\n+\n+\tinfo-\u003ekey.ip_version = 6;\n+\tinfo-\u003ekey.dscp = ipv6_get_dsfield(ip6h) \u003e\u003e 2;\n+\tinfo-\u003ekey.flow_label = ip6_flowlabel(ip6h);\n+\tinfo-\u003ekey.src = ip6h-\u003esaddr;\n+\tinfo-\u003ekey.dst = ip6h-\u003edaddr;\n+\n+\t/* The classifier matches Next Header, not the layer 4 protocol */\n+\tinfo-\u003ekey.proto = ip6h-\u003enexthdr;\n+\tinfo-\u003epresent |= FLOW_F(IP_VERSION) | FLOW_F(IP_SRC) | FLOW_F(IP_DST) |\n+\t\t\t FLOW_F(DSCP) | FLOW_F(FLOW_LABEL) | FLOW_F(PROTO);\n+\n+\tnexthdr = ip6h-\u003enexthdr;\n+\tl4_off = ipv6_skip_exthdr(skb, offset + sizeof(*ip6h), \u0026nexthdr,\n+\t\t\t\t \u0026frag_off);\n+\tif (l4_off \u003c 0 || frag_off \u0026 htons(IP6_OFFSET))\n+\t\treturn;\n+\n+\tcfg80211_flow_parse_ports(skb, l4_off, nexthdr, info);\n+}\n+\n+/**\n+ * cfg80211_flow_parse - read the classifier parameters of an MSDU\n+ *\n+ * @skb: the frame, in IEEE 802.3 format\n+ * @info: receives the parameters\n+ *\n+ * Parses from skb-\u003edata, since a frame forwarded between two stations\n+ * arrives with skb-\u003eprotocol set to ETH_P_802_3 and no network header.\n+ *\n+ * Return: %true when @info describes the frame.\n+ */\n+bool cfg80211_flow_parse(struct sk_buff *skb, struct cfg80211_flow_info *info)\n+{\n+\tstruct ethhdr eth_buf, *eth;\n+\tunsigned int offset = ETH_HLEN;\n+\t__be16 proto;\n+\n+\tmemset(info, 0, sizeof(*info));\n+\n+\teth = skb_header_pointer(skb, 0, sizeof(*eth), \u0026eth_buf);\n+\tif (!eth)\n+\t\treturn false;\n+\n+\tether_addr_copy(info-\u003ekey.sa, eth-\u003eh_source);\n+\tether_addr_copy(info-\u003ekey.da, eth-\u003eh_dest);\n+\tinfo-\u003epresent = FLOW_F(ETH_SA) | FLOW_F(ETH_DA);\n+\tproto = eth-\u003eh_proto;\n+\n+\tif (skb_vlan_tag_present(skb)) {\n+\t\tinfo-\u003ekey.vlan_tci = htons(skb_vlan_tag_get(skb));\n+\t\tinfo-\u003epresent |= FLOW_F_VLAN;\n+\t} else if (eth_type_vlan(proto)) {\n+\t\tstruct vlan_hdr vhdr_buf, *vhdr;\n+\n+\t\tvhdr = skb_header_pointer(skb, offset, sizeof(*vhdr),\n+\t\t\t\t\t \u0026vhdr_buf);\n+\t\tif (!vhdr)\n+\t\t\treturn false;\n+\n+\t\tinfo-\u003ekey.vlan_tci = vhdr-\u003eh_vlan_TCI;\n+\t\tinfo-\u003epresent |= FLOW_F_VLAN;\n+\t\tproto = vhdr-\u003eh_vlan_encapsulated_proto;\n+\t\toffset += sizeof(*vhdr);\n+\t}\n+\n+\t/* Use the type behind the VLAN tag, whether it was stripped or not */\n+\tinfo-\u003ekey.eth_type = proto;\n+\tinfo-\u003epresent |= FLOW_F(ETH_TYPE);\n+\n+\tinfo-\u003eskb = skb;\n+\tinfo-\u003el3_off = offset;\n+\n+\tswitch (proto) {\n+\tcase htons(ETH_P_IP):\n+\t\tcfg80211_flow_parse_ipv4(skb, offset, info);\n+\t\tbreak;\n+\tcase htons(ETH_P_IPV6):\n+\t\tcfg80211_flow_parse_ipv6(skb, offset, info);\n+\t\tbreak;\n+\t}\n+\n+\treturn true;\n+}\n+EXPORT_SYMBOL(cfg80211_flow_parse);\n+\n+static void flow_key_swap(struct cfg80211_flow_key *k)\n+{\n+\tu8 addr[ETH_ALEN];\n+\n+\tmemcpy(addr, k-\u003esa, ETH_ALEN);\n+\tmemcpy(k-\u003esa, k-\u003eda, ETH_ALEN);\n+\tmemcpy(k-\u003eda, addr, ETH_ALEN);\n+\n+\tswap(k-\u003esrc, k-\u003edst);\n+\tswap(k-\u003esrc_port, k-\u003edst_port);\n+}\n+\n+static u32 cfg80211_flow_fields_mirror(u32 fields)\n+{\n+\tstatic const u8 pairs[][2] = {\n+\t\t{ CFG80211_FLOW_F_ETH_SA, CFG80211_FLOW_F_ETH_DA },\n+\t\t{ CFG80211_FLOW_F_IP_SRC, CFG80211_FLOW_F_IP_DST },\n+\t\t{ CFG80211_FLOW_F_SRC_PORT, CFG80211_FLOW_F_DST_PORT },\n+\t};\n+\tunsigned int i;\n+\tu32 out = fields;\n+\n+\tfor (i = 0; i \u003c ARRAY_SIZE(pairs); i++) {\n+\t\tu32 lo = BIT(pairs[i][0]), hi = BIT(pairs[i][1]);\n+\n+\t\tout \u0026= ~(lo | hi);\n+\t\tif (fields \u0026 lo)\n+\t\t\tout |= hi;\n+\t\tif (fields \u0026 hi)\n+\t\t\tout |= lo;\n+\t}\n+\n+\treturn out;\n+}\n+\n+/**\n+ * cfg80211_flow_key_build - build an MSCS lookup key\n+ *\n+ * @info: parsed frame, from cfg80211_flow_parse()\n+ * @fields: classifier parameters of the MSCS, a bitmap of\n+ *\t\u0026enum cfg80211_flow_field\n+ * @dir: %CFG80211_FLOW_MIRRORED when @info describes an uplink frame and the\n+ *\tkey must describe the downlink direction\n+ * @key: receives the key\n+ *\n+ * Everything that @fields does not select stays zero, so the key is hashed and\n+ * compared as a byte string.\n+ *\n+ * Return: %false when @info lacks a parameter that @fields selects, in which\n+ *\tcase the MSDU is not classified at all.\n+ */\n+bool cfg80211_flow_key_build(const struct cfg80211_flow_info *info, u32 fields,\n+\t\t\t enum cfg80211_flow_dir dir,\n+\t\t\t struct cfg80211_flow_key *key)\n+{\n+\tstruct cfg80211_flow_key mirrored;\n+\n+\tif (dir == CFG80211_FLOW_AS_IS) {\n+\t\tif (fields \u0026 ~info-\u003epresent)\n+\t\t\treturn false;\n+\n+\t\tflow_key_select(\u0026info-\u003ekey, fields, key);\n+\n+\t\treturn true;\n+\t}\n+\n+\tif (cfg80211_flow_fields_mirror(fields) \u0026 ~info-\u003epresent)\n+\t\treturn false;\n+\n+\tmirrored = info-\u003ekey;\n+\tflow_key_swap(\u0026mirrored);\n+\tflow_key_select(\u0026mirrored, fields, key);\n+\n+\treturn true;\n+}\n+EXPORT_SYMBOL(cfg80211_flow_key_build);\n+\n+/* Find the payload behind the @instance-th occurrence of the protocol */\n+static bool tclas_filter_offset(const struct cfg80211_flow_info *info,\n+\t\t\t\tconst struct cfg80211_tclas_filter *f,\n+\t\t\t\tunsigned int *off)\n+{\n+\tstruct ipv6hdr ip6h_buf, *ip6h;\n+\tunsigned int pos = info-\u003el3_off;\n+\tunsigned int hdrs = 0;\n+\tu8 instance = 0;\n+\tu8 nexthdr;\n+\n+\tif (info-\u003ekey.ip_version == 4) {\n+\t\tstruct iphdr iph_buf, *iph;\n+\n+\t\tiph = skb_header_pointer(info-\u003eskb, pos, sizeof(*iph),\n+\t\t\t\t\t \u0026iph_buf);\n+\t\tif (!iph || iph-\u003eprotocol != f-\u003eprotocol || f-\u003einstance)\n+\t\t\treturn false;\n+\n+\t\t*off = pos + iph-\u003eihl * 4;\n+\n+\t\treturn true;\n+\t}\n+\n+\tif (info-\u003ekey.ip_version != 6)\n+\t\treturn false;\n+\n+\tip6h = skb_header_pointer(info-\u003eskb, pos, sizeof(*ip6h), \u0026ip6h_buf);\n+\tif (!ip6h)\n+\t\treturn false;\n+\n+\tnexthdr = ip6h-\u003enexthdr;\n+\tpos += sizeof(*ip6h);\n+\n+\twhile (hdrs++ \u003c 8) {\n+\t\tstruct ipv6_opt_hdr hdr_buf, *hdr;\n+\n+\t\tif (nexthdr == f-\u003eprotocol \u0026\u0026 instance++ == f-\u003einstance) {\n+\t\t\t*off = pos;\n+\n+\t\t\treturn true;\n+\t\t}\n+\n+\t\tif (!ipv6_ext_hdr(nexthdr) || nexthdr == NEXTHDR_NONE)\n+\t\t\treturn false;\n+\n+\t\thdr = skb_header_pointer(info-\u003eskb, pos, sizeof(*hdr),\n+\t\t\t\t\t \u0026hdr_buf);\n+\t\tif (!hdr)\n+\t\t\treturn false;\n+\n+\t\tif (nexthdr == NEXTHDR_FRAGMENT)\n+\t\t\tpos += 8;\n+\t\telse if (nexthdr == NEXTHDR_AUTH)\n+\t\t\tpos += ipv6_authlen(hdr);\n+\t\telse\n+\t\t\tpos += ipv6_optlen(hdr);\n+\n+\t\tnexthdr = hdr-\u003enexthdr;\n+\t}\n+\n+\treturn false;\n+}\n+\n+static bool tclas_filter_match(const struct cfg80211_tclas *t,\n+\t\t\t const struct cfg80211_flow_info *info)\n+{\n+\tconst struct cfg80211_tclas_filter *f = \u0026t-\u003efilter;\n+\tu8 buf[CFG80211_TCLAS_FILTER_LEN], *p;\n+\tunsigned int i, off;\n+\n+\tif (!info-\u003eskb)\n+\t\treturn false;\n+\n+\tif (!tclas_filter_offset(info, f, \u0026off))\n+\t\treturn false;\n+\n+\tp = skb_header_pointer(info-\u003eskb, off, f-\u003elen, buf);\n+\tif (!p)\n+\t\treturn false;\n+\n+\tfor (i = 0; i \u003c f-\u003elen; i++)\n+\t\tif ((p[i] ^ f-\u003evalue[i]) \u0026 f-\u003emask[i])\n+\t\t\treturn false;\n+\n+\treturn true;\n+}\n+\n+static bool tclas_match(const struct cfg80211_tclas *t,\n+\t\t\tconst struct cfg80211_flow_info *info)\n+{\n+\tstruct cfg80211_flow_key key;\n+\n+\tif (t-\u003etype == CFG80211_TCLAS_FILTER)\n+\t\treturn tclas_filter_match(t, info);\n+\n+\tif (!cfg80211_flow_key_build(info, t-\u003efields, CFG80211_FLOW_AS_IS,\n+\t\t\t\t \u0026key))\n+\t\treturn false;\n+\n+\treturn !memcmp(\u0026key, \u0026t-\u003ekey, sizeof(key));\n+}\n+\n+/* Number of classifier parameters required for a match, see 11.25.2 */\n+static u16 tclas_param_count(const struct cfg80211_tclas *t)\n+{\n+\tif (t-\u003etype == CFG80211_TCLAS_FILTER)\n+\t\treturn 1;\n+\n+\tif (t-\u003etype == CFG80211_TCLAS_VLAN)\n+\t\treturn 3;\n+\n+\treturn hweight32(t-\u003efields \u0026 ~FLOW_F(IP_VERSION));\n+}\n+\n+static bool scs_desc_match(const struct cfg80211_scs_desc *desc,\n+\t\t\t const struct cfg80211_flow_info *info)\n+{\n+\tunsigned int i;\n+\tbool any = false;\n+\n+\tfor (i = 0; i \u003c desc-\u003en_tclas; i++) {\n+\t\tif (tclas_match(\u0026desc-\u003etclas[i], info)) {\n+\t\t\tany = true;\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\tif (desc-\u003etclas_processing == CFG80211_TCLAS_PROCESSING_ALL)\n+\t\t\treturn false;\n+\t}\n+\n+\treturn any;\n+}\n+\n+static u16 scs_desc_param_count(const struct cfg80211_scs_desc *desc)\n+{\n+\tunsigned int i;\n+\tu16 count;\n+\n+\tif (desc-\u003etclas_processing != CFG80211_TCLAS_PROCESSING_ALL) {\n+\t\tcount = U16_MAX;\n+\t\tfor (i = 0; i \u003c desc-\u003en_tclas; i++)\n+\t\t\tcount = min(count, tclas_param_count(\u0026desc-\u003etclas[i]));\n+\n+\t\treturn count == U16_MAX ? 0 : count;\n+\t}\n+\n+\tcount = 0;\n+\tfor (i = 0; i \u003c desc-\u003en_tclas; i++)\n+\t\tcount += tclas_param_count(\u0026desc-\u003etclas[i]);\n+\n+\treturn count;\n+}\n+\n+/**\n+ * cfg80211_scs_evaluate - pick the SCS descriptor that matches a flow\n+ *\n+ * @desc: the active descriptors of one peer\n+ * @n_desc: number of entries in @desc\n+ * @info: the flow, from cfg80211_flow_parse()\n+ * @verdict: receives the result\n+ *\n+ * When several descriptors match, the one with the most classifier parameters\n+ * wins. An equal count keeps the first.\n+ */\n+void cfg80211_scs_evaluate(struct cfg80211_scs_desc * const *desc, u8 n_desc,\n+\t\t\t const struct cfg80211_flow_info *info,\n+\t\t\t struct cfg80211_scs_verdict *verdict)\n+{\n+\tunsigned int i;\n+\tint best = -1;\n+\n+\tmemset(verdict, 0, sizeof(*verdict));\n+\n+\tfor (i = 0; i \u003c n_desc; i++) {\n+\t\tu16 count;\n+\n+\t\tif (!scs_desc_match(desc[i], info))\n+\t\t\tcontinue;\n+\n+\t\tcount = scs_desc_param_count(desc[i]);\n+\t\tif (count \u003c= best)\n+\t\t\tcontinue;\n+\n+\t\tbest = count;\n+\t\tverdict-\u003ematch = true;\n+\t\tverdict-\u003escsid = desc[i]-\u003eid;\n+\t\tverdict-\u003eup = desc[i]-\u003eup;\n+\t}\n+}\n+EXPORT_SYMBOL(cfg80211_scs_evaluate);\n+\n+/* Two elements of a match-all descriptor can require conflicting values */\n+static bool tclas_conflict(const struct cfg80211_tclas *a,\n+\t\t\t const struct cfg80211_tclas *b)\n+{\n+\tstruct cfg80211_flow_key ka, kb;\n+\tu32 common = a-\u003efields \u0026 b-\u003efields;\n+\n+\tif (!common)\n+\t\treturn false;\n+\n+\tflow_key_select(\u0026a-\u003ekey, common, \u0026ka);\n+\tflow_key_select(\u0026b-\u003ekey, common, \u0026kb);\n+\n+\treturn memcmp(\u0026ka, \u0026kb, sizeof(ka));\n+}\n+\n+/**\n+ * cfg80211_scs_desc_valid - validate one SCS descriptor\n+ *\n+ * @desc: the descriptor, already parsed\n+ *\n+ * Checks the constraints between the request type, the classifier and the\n+ * traffic description. cfg80211_parse_tclas() validates each element.\n+ *\n+ * Return: whether the descriptor may be installed. The caller reports a\n+ *\trefusal as a status code, not as an error.\n+ */\n+bool cfg80211_scs_desc_valid(const struct cfg80211_scs_desc *desc)\n+{\n+\tbool want_tclas = true;\n+\tunsigned int i, j;\n+\n+\tif (desc-\u003ereq_type == NL80211_SCS_REQ_REMOVE)\n+\t\treturn true;\n+\n+\t/* Only a downlink stream is classified by the AP */\n+\tif (desc-\u003eqos_char \u0026\u0026\n+\t ieee80211_qos_char_direction(desc-\u003eqos_char) !=\n+\t IEEE80211_QOS_CHAR_DIR_DOWNLINK)\n+\t\twant_tclas = false;\n+\n+\tif (want_tclas != !!desc-\u003en_tclas)\n+\t\treturn false;\n+\n+\tif (!want_tclas)\n+\t\treturn desc-\u003etclas_processing ==\n+\t\t CFG80211_TCLAS_PROCESSING_ABSENT;\n+\n+\tswitch (desc-\u003etclas_processing) {\n+\tcase CFG80211_TCLAS_PROCESSING_ALL:\n+\tcase CFG80211_TCLAS_PROCESSING_ANY:\n+\t\tif (desc-\u003en_tclas \u003c 2)\n+\t\t\treturn false;\n+\t\tbreak;\n+\tcase CFG80211_TCLAS_PROCESSING_DEFAULT:\n+\t\treturn false;\n+\tcase CFG80211_TCLAS_PROCESSING_ABSENT:\n+\t\tif (desc-\u003en_tclas \u003e 1)\n+\t\t\treturn false;\n+\t\tbreak;\n+\tdefault:\n+\t\treturn false;\n+\t}\n+\n+\tif (desc-\u003etclas_processing != CFG80211_TCLAS_PROCESSING_ALL)\n+\t\treturn true;\n+\n+\tfor (i = 0; i \u003c desc-\u003en_tclas; i++)\n+\t\tfor (j = i + 1; j \u003c desc-\u003en_tclas; j++)\n+\t\t\tif (tclas_conflict(\u0026desc-\u003etclas[i], \u0026desc-\u003etclas[j]))\n+\t\t\t\treturn false;\n+\n+\treturn true;\n+}\n+EXPORT_SYMBOL_IF_CFG80211_KUNIT(cfg80211_scs_desc_valid);\n+\n+/**\n+ * cfg80211_mscs_desc_valid - validate an MSCS descriptor\n+ *\n+ * @desc: the descriptor, already parsed\n+ *\n+ * Return: whether the descriptor may be installed.\n+ */\n+bool cfg80211_mscs_desc_valid(const struct cfg80211_mscs_desc *desc)\n+{\n+\tbool remove = desc-\u003ereq_type == NL80211_SCS_REQ_REMOVE;\n+\n+\tif (remove == !!desc-\u003efields)\n+\t\treturn false;\n+\n+\t/* A zero timeout would expire every entry at the next collection */\n+\treturn remove || desc-\u003estream_timeout;\n+}\ndiff --git a/net/wireless/tests/Makefile b/net/wireless/tests/Makefile\nindex c364e63b508eb..a4b7f2e206fc2 100644\n--- a/net/wireless/tests/Makefile\n+++ b/net/wireless/tests/Makefile\n@@ -1,3 +1,3 @@\n-cfg80211-tests-y += module.o fragmentation.o scan.o util.o chan.o\n+cfg80211-tests-y += module.o fragmentation.o scan.o util.o chan.o scs.o\n \n obj-$(CONFIG_CFG80211_KUNIT_TEST) += cfg80211-tests.o\ndiff --git a/net/wireless/tests/scs.c b/net/wireless/tests/scs.c\nnew file mode 100644\nindex 0000000000000..d3636bd459644\n--- /dev/null\n+++ b/net/wireless/tests/scs.c\n@@ -0,0 +1,488 @@\n+// SPDX-License-Identifier: GPL-2.0-only\n+/*\n+ * KUnit tests for the SCS and MSCS classifier\n+ *\n+ * Copyright (C) 2026 Felix Fietkau \u003cnbd@nbd.name\u003e\n+ */\n+#include \u003ckunit/skbuff.h\u003e\n+#include \u003ckunit/test.h\u003e\n+#include \u003clinux/ieee80211.h\u003e\n+#include \u003clinux/ip.h\u003e\n+#include \u003cnet/cfg80211.h\u003e\n+#include \"../core.h\"\n+\n+static const u8 test_da[ETH_ALEN] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x02 };\n+\n+/* An Ethernet header, an IPv4 header and four octets of UDP ports */\n+static const u8 udp_frame[] = {\n+\t0x02, 0x00, 0x00, 0x00, 0x00, 0x02,\t/* destination address */\n+\t0x02, 0x00, 0x00, 0x00, 0x00, 0x01,\t/* source address */\n+\t0x08, 0x00,\n+\t0x45, 0x00, 0x00, 0x20, 0xab, 0xcd, 0x00, 0x00,\n+\t0x40, 0x11, 0x00, 0x00,\n+\t192, 168, 1, 1,\n+\t192, 168, 1, 2,\n+\t0x13, 0x88, 0x27, 0x10,\n+\t0xde, 0xad, 0xbe, 0xef,\n+};\n+\n+/* The same datagram, as a fragment with a nonzero offset */\n+static const u8 udp_fragment[] = {\n+\t0x02, 0x00, 0x00, 0x00, 0x00, 0x02,\t/* destination address */\n+\t0x02, 0x00, 0x00, 0x00, 0x00, 0x01,\t/* source address */\n+\t0x08, 0x00,\n+\t0x45, 0x00, 0x00, 0x20, 0xab, 0xcd, 0x00, 0x02,\n+\t0x40, 0x11, 0x00, 0x00,\n+\t192, 168, 1, 1,\n+\t192, 168, 1, 2,\n+\t0x13, 0x88, 0x27, 0x10,\n+\t0xde, 0xad, 0xbe, 0xef,\n+};\n+\n+static struct sk_buff *test_skb(struct kunit *test, const u8 *data,\n+\t\t\t\tsize_t len)\n+{\n+\tstruct sk_buff *skb = kunit_zalloc_skb(test, 256, GFP_KERNEL);\n+\n+\tKUNIT_ASSERT_NOT_NULL(test, skb);\n+\tskb_put_data(skb, data, len);\n+\n+\treturn skb;\n+}\n+\n+#define TEST_SKB(test, buf)\ttest_skb(test, buf, sizeof(buf))\n+\n+#define FLOW_F(name)\tBIT(CFG80211_FLOW_F_##name)\n+\n+#define QC(buf)\t\t((const struct ieee80211_qos_char_elem *)(buf))\n+#define QC_OK(buf)\tieee80211_qos_char_size_ok(buf, sizeof(buf))\n+\n+/* A later fragment carries no ports */\n+static void flow_parse_fragment(struct kunit *test)\n+{\n+\tstruct cfg80211_flow_info info;\n+\n+\tKUNIT_ASSERT_TRUE(test,\n+\t\t\t cfg80211_flow_parse(TEST_SKB(test, udp_frame),\n+\t\t\t\t\t \u0026info));\n+\tKUNIT_EXPECT_TRUE(test, info.present \u0026 FLOW_F(SRC_PORT));\n+\tKUNIT_EXPECT_TRUE(test, info.present \u0026 FLOW_F(DST_PORT));\n+\tKUNIT_EXPECT_EQ(test, be16_to_cpu(info.key.src_port), 5000);\n+\tKUNIT_EXPECT_EQ(test, be16_to_cpu(info.key.dst_port), 10000);\n+\tKUNIT_EXPECT_EQ(test, info.key.proto, IPPROTO_UDP);\n+\n+\tKUNIT_ASSERT_TRUE(test,\n+\t\t\t cfg80211_flow_parse(TEST_SKB(test, udp_fragment),\n+\t\t\t\t\t \u0026info));\n+\tKUNIT_EXPECT_FALSE(test, info.present \u0026 FLOW_F(SRC_PORT));\n+\tKUNIT_EXPECT_FALSE(test, info.present \u0026 FLOW_F(DST_PORT));\n+\tKUNIT_EXPECT_EQ(test, be16_to_cpu(info.key.src_port), 0);\n+\tKUNIT_EXPECT_EQ(test, be16_to_cpu(info.key.dst_port), 0);\n+\tKUNIT_EXPECT_TRUE(test, info.present \u0026 FLOW_F(IP_SRC));\n+\tKUNIT_EXPECT_TRUE(test, info.present \u0026 FLOW_F(IP_DST));\n+}\n+\n+static struct cfg80211_scs_desc *\n+test_scs_desc(struct kunit *test, u8 id, u8 up, const u8 *elems, size_t len)\n+{\n+\tstruct cfg80211_scs_desc *desc;\n+\tint n_tclas;\n+\n+\tn_tclas = cfg80211_tclas_count(elems, len);\n+\tKUNIT_ASSERT_GE(test, n_tclas, 0);\n+\n+\tdesc = kunit_kzalloc(test, struct_size(desc, tclas, n_tclas),\n+\t\t\t GFP_KERNEL);\n+\tKUNIT_ASSERT_NOT_NULL(test, desc);\n+\n+\tdesc-\u003eid = id;\n+\tdesc-\u003eup = up;\n+\tdesc-\u003en_tclas = n_tclas;\n+\tKUNIT_ASSERT_EQ(test, 0,\n+\t\t\tcfg80211_parse_tclas(elems, len, desc-\u003etclas, n_tclas,\n+\t\t\t\t\t \u0026desc-\u003etclas_processing));\n+\n+\treturn desc;\n+}\n+\n+/* When two descriptors match, the more granular one wins */\n+static void scs_granularity(struct kunit *test)\n+{\n+\tstatic const u8 two_params[] = {\n+\t\tWLAN_EID_TCLAS, 19,\n+\t\t255,\n+\t\t4, 0x06,\t\t/* type, mask: src and dst address */\n+\t\t4,\t\t\t/* version */\n+\t\t192, 168, 1, 1,\n+\t\t192, 168, 1, 2,\n+\t\t0, 0, 0, 0,\t\t/* ports, not selected */\n+\t\t0, 0, 0,\t\t/* dscp, protocol, reserved */\n+\t};\n+\tstatic const u8 three_params[] = {\n+\t\tWLAN_EID_TCLAS, 19,\n+\t\t255,\n+\t\t4, 0x16,\t\t/* src, dst, dst port */\n+\t\t4,\n+\t\t192, 168, 1, 1,\n+\t\t192, 168, 1, 2,\n+\t\t0, 0, 0x27, 0x10,\n+\t\t0, 0, 0,\n+\t};\n+\tstatic const u8 wrong_port[] = {\n+\t\tWLAN_EID_TCLAS, 19,\n+\t\t255,\n+\t\t4, 0x08,\t\t/* src port only */\n+\t\t4,\n+\t\t0, 0, 0, 0,\n+\t\t0, 0, 0, 0,\n+\t\t0x00, 0x35, 0, 0,\n+\t\t0, 0, 0,\n+\t};\n+\tstruct cfg80211_scs_desc *list[3];\n+\tstruct cfg80211_scs_verdict verdict;\n+\tstruct cfg80211_flow_info info;\n+\n+\tlist[0] = test_scs_desc(test, 1, 3, two_params, sizeof(two_params));\n+\tlist[1] = test_scs_desc(test, 2, 6, three_params, sizeof(three_params));\n+\tlist[2] = test_scs_desc(test, 3, 7, wrong_port, sizeof(wrong_port));\n+\n+\tKUNIT_ASSERT_TRUE(test,\n+\t\t\t cfg80211_flow_parse(TEST_SKB(test, udp_frame),\n+\t\t\t\t\t \u0026info));\n+\n+\t/* The IP version is always matched, but does not count as a parameter */\n+\tKUNIT_EXPECT_EQ(test, list[0]-\u003etclas[0].fields,\n+\t\t\tFLOW_F(IP_VERSION) | FLOW_F(IP_SRC) | FLOW_F(IP_DST));\n+\tKUNIT_EXPECT_EQ(test, list[1]-\u003etclas[0].fields,\n+\t\t\tFLOW_F(IP_VERSION) | FLOW_F(IP_SRC) | FLOW_F(IP_DST) |\n+\t\t\tFLOW_F(DST_PORT));\n+\n+\tcfg80211_scs_evaluate(list, 3, \u0026info, \u0026verdict);\n+\tKUNIT_EXPECT_TRUE(test, verdict.match);\n+\tKUNIT_EXPECT_EQ(test, verdict.scsid, 2);\n+\tKUNIT_EXPECT_EQ(test, verdict.up, 6);\n+\n+\tswap(list[0], list[1]);\n+\tcfg80211_scs_evaluate(list, 3, \u0026info, \u0026verdict);\n+\tKUNIT_EXPECT_TRUE(test, verdict.match);\n+\tKUNIT_EXPECT_EQ(test, verdict.scsid, 2);\n+\n+\t/* A frame without ports leaves only the address descriptor */\n+\tKUNIT_ASSERT_TRUE(test,\n+\t\t\t cfg80211_flow_parse(TEST_SKB(test, udp_fragment),\n+\t\t\t\t\t \u0026info));\n+\tcfg80211_scs_evaluate(list, 3, \u0026info, \u0026verdict);\n+\tKUNIT_EXPECT_TRUE(test, verdict.match);\n+\tKUNIT_EXPECT_EQ(test, verdict.scsid, 1);\n+\tKUNIT_EXPECT_EQ(test, verdict.up, 3);\n+}\n+\n+/* A key learned from an uplink frame must describe the downlink direction */\n+static void flow_key_mirror(struct kunit *test)\n+{\n+\tstatic const u8 mask_elem[] = {\n+\t\tWLAN_EID_EXTENSION, 19,\n+\t\tWLAN_EID_EXT_TCLAS_MASK,\n+\t\t4, 0x1e,\n+\t\t4,\n+\t\t0, 0, 0, 0,\n+\t\t0, 0, 0, 0,\n+\t\t0, 0, 0, 0,\n+\t\t0, 0, 0,\n+\t};\n+\tstruct cfg80211_flow_key as_is, mirrored;\n+\tstruct cfg80211_flow_info info;\n+\tu32 fields;\n+\n+\tKUNIT_ASSERT_EQ(test, 0,\n+\t\t\tcfg80211_parse_tclas_mask(mask_elem, sizeof(mask_elem),\n+\t\t\t\t\t\t \u0026fields));\n+\tKUNIT_EXPECT_EQ(test, fields,\n+\t\t\tFLOW_F(IP_VERSION) | FLOW_F(IP_SRC) | FLOW_F(IP_DST) |\n+\t\t\tFLOW_F(SRC_PORT) | FLOW_F(DST_PORT));\n+\n+\tKUNIT_ASSERT_TRUE(test,\n+\t\t\t cfg80211_flow_parse(TEST_SKB(test, udp_frame),\n+\t\t\t\t\t \u0026info));\n+\n+\tKUNIT_ASSERT_TRUE(test, cfg80211_flow_key_build(\u0026info, fields,\n+\t\t\t\t\t\t\tCFG80211_FLOW_AS_IS,\n+\t\t\t\t\t\t\t\u0026as_is));\n+\tKUNIT_ASSERT_TRUE(test, cfg80211_flow_key_build(\u0026info, fields,\n+\t\t\t\t\t\t\tCFG80211_FLOW_MIRRORED,\n+\t\t\t\t\t\t\t\u0026mirrored));\n+\n+\tKUNIT_EXPECT_MEMEQ(test, \u0026as_is.src, \u0026mirrored.dst, sizeof(as_is.src));\n+\tKUNIT_EXPECT_MEMEQ(test, \u0026as_is.dst, \u0026mirrored.src, sizeof(as_is.dst));\n+\tKUNIT_EXPECT_EQ(test, be16_to_cpu(as_is.src_port),\n+\t\t\tbe16_to_cpu(mirrored.dst_port));\n+\tKUNIT_EXPECT_EQ(test, be16_to_cpu(as_is.dst_port),\n+\t\t\tbe16_to_cpu(mirrored.src_port));\n+\n+\t/* No unselected field may reach the key */\n+\tKUNIT_EXPECT_MEMEQ(test, as_is.sa, \"\\x00\\x00\\x00\\x00\\x00\\x00\",\n+\t\t\t ETH_ALEN);\n+\tKUNIT_EXPECT_EQ(test, be16_to_cpu(as_is.eth_type), 0);\n+\tKUNIT_EXPECT_EQ(test, as_is.proto, 0);\n+\tKUNIT_EXPECT_EQ(test, as_is.ip_version, 4);\n+\tKUNIT_EXPECT_EQ(test, mirrored.ip_version, 4);\n+}\n+\n+/* The Ethernet addresses mirror as a pair */\n+static void flow_key_mirror_eth(struct kunit *test)\n+{\n+\tstatic const u8 mask_elem[] = {\n+\t\tWLAN_EID_EXTENSION, 17,\n+\t\tWLAN_EID_EXT_TCLAS_MASK,\n+\t\t0, 0x01,\t\t/* classifier type 0, source address */\n+\t\t0, 0, 0, 0, 0, 0,\n+\t\t0, 0, 0, 0, 0, 0,\n+\t\t0, 0,\n+\t};\n+\tstruct cfg80211_flow_key key;\n+\tstruct cfg80211_flow_info info;\n+\tu32 fields;\n+\n+\tKUNIT_ASSERT_EQ(test, 0,\n+\t\t\tcfg80211_parse_tclas_mask(mask_elem, sizeof(mask_elem),\n+\t\t\t\t\t\t \u0026fields));\n+\tKUNIT_EXPECT_EQ(test, fields, FLOW_F(ETH_SA));\n+\n+\tKUNIT_ASSERT_TRUE(test,\n+\t\t\t cfg80211_flow_parse(TEST_SKB(test, udp_frame),\n+\t\t\t\t\t \u0026info));\n+\n+\tKUNIT_ASSERT_TRUE(test, cfg80211_flow_key_build(\u0026info, fields,\n+\t\t\t\t\t\t\tCFG80211_FLOW_MIRRORED,\n+\t\t\t\t\t\t\t\u0026key));\n+\n+\tKUNIT_EXPECT_MEMEQ(test, key.sa, test_da, ETH_ALEN);\n+\tKUNIT_EXPECT_MEMEQ(test, key.da, \"\\x00\\x00\\x00\\x00\\x00\\x00\", ETH_ALEN);\n+}\n+\n+/* Each unsupported classifier is refused with -EOPNOTSUPP, not -EINVAL */\n+static void tclas_refused(struct kunit *test)\n+{\n+\tstatic const u8 mac_header[] = {\n+\t\tWLAN_EID_TCLAS, 5,\n+\t\t255,\n+\t\t6, 0x00, 0x00, 0x00,\t/* type 6, 3 octet mask */\n+\t};\n+\tstatic const u8 up_not_255[] = {\n+\t\tWLAN_EID_TCLAS, 17,\n+\t\t0,\t\t\t/* a UP that takes part in the match */\n+\t\t0, 0x01,\n+\t\t0x02, 0x00, 0x00, 0x00, 0x00, 0x01,\n+\t\t0x02, 0x00, 0x00, 0x00, 0x00, 0x02,\n+\t\t0x08, 0x00,\n+\t};\n+\tstatic const u8 filter_in_mask[] = {\n+\t\tWLAN_EID_EXTENSION, 9,\n+\t\tWLAN_EID_EXT_TCLAS_MASK,\n+\t\t3, 0,\n+\t\t0, 0,\n+\t\t0x00, 0x00,\n+\t\t0xff, 0xff,\n+\t};\n+\tstatic const u8 legacy_ip[] = {\n+\t\tWLAN_EID_TCLAS, 19,\n+\t\t255,\n+\t\t1, 0x06,\n+\t\t4,\n+\t\t192, 168, 1, 1,\n+\t\t192, 168, 1, 2,\n+\t\t0, 0, 0, 0,\n+\t\t0, 0, 0,\n+\t};\n+\tstatic const u8 legacy_vlan[] = {\n+\t\tWLAN_EID_TCLAS, 5,\n+\t\t255,\n+\t\t2, 0x01,\n+\t\t0x00, 0x64,\n+\t};\n+\tenum cfg80211_tclas_processing processing;\n+\tstruct cfg80211_tclas tclas;\n+\tu32 fields;\n+\n+\tKUNIT_EXPECT_EQ(test, -EOPNOTSUPP,\n+\t\t\tcfg80211_parse_tclas(mac_header, sizeof(mac_header),\n+\t\t\t\t\t \u0026tclas, 1, \u0026processing));\n+\tKUNIT_EXPECT_EQ(test, -EOPNOTSUPP,\n+\t\t\tcfg80211_parse_tclas(up_not_255, sizeof(up_not_255),\n+\t\t\t\t\t \u0026tclas, 1, \u0026processing));\n+\tKUNIT_EXPECT_EQ(test, -EOPNOTSUPP,\n+\t\t\tcfg80211_parse_tclas(legacy_ip, sizeof(legacy_ip),\n+\t\t\t\t\t \u0026tclas, 1, \u0026processing));\n+\tKUNIT_EXPECT_EQ(test, -EOPNOTSUPP,\n+\t\t\tcfg80211_parse_tclas(legacy_vlan, sizeof(legacy_vlan),\n+\t\t\t\t\t \u0026tclas, 1, \u0026processing));\n+\tKUNIT_EXPECT_EQ(test, -EOPNOTSUPP,\n+\t\t\tcfg80211_parse_tclas_mask(filter_in_mask,\n+\t\t\t\t\t\t sizeof(filter_in_mask),\n+\t\t\t\t\t\t \u0026fields));\n+}\n+\n+/* The presence bitmap decides where every optional field starts */\n+static void qos_characteristics(struct kunit *test)\n+{\n+\t/* Downlink, TID 5, UP 5, and a Service Start Time with no LinkID */\n+\tstatic const u8 no_link_id[] = {\n+\t\t0xff, 0x17, 0x71,\t/* element header, 113 */\n+\t\t0x55, 0x05, 0x00, 0x00,\t/* control info, presence bit 1 */\n+\t\t0xe8, 0x03, 0x00, 0x00,\t/* minimum service interval */\n+\t\t0xd0, 0x07, 0x00, 0x00,\t/* maximum service interval */\n+\t\t0x88, 0x13, 0x00,\t/* minimum data rate */\n+\t\t0x20, 0x4e, 0x00,\t/* delay bound */\n+\t\t0x78, 0x56, 0x34, 0x12,\t/* service start time */\n+\t};\n+\tstatic const u8 with_link_id[] = {\n+\t\t0xff, 0x18, 0x71,\n+\t\t0x55, 0x0d, 0x00, 0x00,\t/* presence bits 1 and 2 */\n+\t\t0xe8, 0x03, 0x00, 0x00,\n+\t\t0xd0, 0x07, 0x00, 0x00,\n+\t\t0x88, 0x13, 0x00,\n+\t\t0x20, 0x4e, 0x00,\n+\t\t0x78, 0x56, 0x34, 0x12,\n+\t\t0x03,\t\t\t/* service start time LinkID */\n+\t};\n+\tstatic const u8 orphan_link_id[] = {\n+\t\t0xff, 0x14, 0x71,\n+\t\t0x55, 0x09, 0x00, 0x00,\t/* presence bit 2 alone */\n+\t\t0xe8, 0x03, 0x00, 0x00,\n+\t\t0xd0, 0x07, 0x00, 0x00,\n+\t\t0x88, 0x13, 0x00,\n+\t\t0x20, 0x4e, 0x00,\n+\t\t0x03,\n+\t};\n+\tstatic const u8 bad_ext_id[] = {\n+\t\t0xff, 0x17, 0x70,\n+\t\t0x55, 0x05, 0x00, 0x00,\n+\t\t0xe8, 0x03, 0x00, 0x00,\n+\t\t0xd0, 0x07, 0x00, 0x00,\n+\t\t0x88, 0x13, 0x00,\n+\t\t0x20, 0x4e, 0x00,\n+\t\t0x78, 0x56, 0x34, 0x12,\n+\t};\n+\tstatic const u8 bad_length[] = {\n+\t\t0xff, 0x18, 0x71,\n+\t\t0x55, 0x05, 0x00, 0x00,\n+\t\t0xe8, 0x03, 0x00, 0x00,\n+\t\t0xd0, 0x07, 0x00, 0x00,\n+\t\t0x88, 0x13, 0x00,\n+\t\t0x20, 0x4e, 0x00,\n+\t\t0x78, 0x56, 0x34, 0x12,\n+\t};\n+\tstatic const u8 short_elem[] = {\n+\t\t0xff, 0x13, 0x71,\n+\t\t0x55, 0x05, 0x00, 0x00,\n+\t\t0xe8, 0x03, 0x00, 0x00,\n+\t\t0xd0, 0x07, 0x00, 0x00,\n+\t\t0x88, 0x13, 0x00,\n+\t\t0x20, 0x4e, 0x00,\n+\t};\n+\n+\tKUNIT_EXPECT_TRUE(test, QC_OK(no_link_id));\n+\tKUNIT_EXPECT_EQ(test, ieee80211_qos_char_direction(QC(no_link_id)),\n+\t\t\tIEEE80211_QOS_CHAR_DIR_DOWNLINK);\n+\tKUNIT_EXPECT_EQ(test, ieee80211_qos_char_presence(QC(no_link_id)),\n+\t\t\tIEEE80211_QOS_CHAR_PRES_SERVICE_START_TIME);\n+\n+\tKUNIT_EXPECT_TRUE(test, QC_OK(with_link_id));\n+\tKUNIT_EXPECT_EQ(test, ieee80211_qos_char_presence(QC(with_link_id)),\n+\t\t\tIEEE80211_QOS_CHAR_PRES_SERVICE_START_TIME |\n+\t\t\tIEEE80211_QOS_CHAR_PRES_SERVICE_START_LINK_ID);\n+\n+\tKUNIT_EXPECT_FALSE(test, QC_OK(orphan_link_id));\n+\tKUNIT_EXPECT_FALSE(test, QC_OK(short_elem));\n+\tKUNIT_EXPECT_FALSE(test, QC_OK(bad_ext_id));\n+\tKUNIT_EXPECT_FALSE(test, QC_OK(bad_length));\n+}\n+\n+/* An uplink descriptor carries no classifier and no user priority */\n+static void scs_traffic_description(struct kunit *test)\n+{\n+\t/* Uplink, and a Service Start Time with no LinkID */\n+\tstatic const u8 uplink[] = {\n+\t\t0xff, 0x17, 0x71,\n+\t\t0x54, 0x05, 0x00, 0x00,\n+\t\t0xe8, 0x03, 0x00, 0x00,\n+\t\t0xd0, 0x07, 0x00, 0x00,\n+\t\t0x88, 0x13, 0x00,\n+\t\t0x20, 0x4e, 0x00,\n+\t\t0x78, 0x56, 0x34, 0x12,\n+\t};\n+\tstruct cfg80211_scs_desc desc = {\n+\t\t.req_type = NL80211_SCS_REQ_ADD,\n+\t\t.tclas_processing = CFG80211_TCLAS_PROCESSING_ABSENT,\n+\t\t.qos_char = QC(uplink),\n+\t\t.qos_char_len = sizeof(uplink),\n+\t\t.id = 1,\n+\t};\n+\n+\tKUNIT_ASSERT_TRUE(test, QC_OK(uplink));\n+\tKUNIT_ASSERT_EQ(test, ieee80211_qos_char_direction(QC(uplink)),\n+\t\t\tIEEE80211_QOS_CHAR_DIR_UPLINK);\n+\n+\tKUNIT_EXPECT_TRUE(test, cfg80211_scs_desc_valid(\u0026desc));\n+\n+\t/* Such a descriptor carries no TCLAS Processing element either */\n+\tdesc.tclas_processing = CFG80211_TCLAS_PROCESSING_DEFAULT;\n+\tKUNIT_EXPECT_FALSE(test, cfg80211_scs_desc_valid(\u0026desc));\n+}\n+\n+/* A filter classifier matches on the payload octets of the named protocol */\n+static void scs_filter(struct kunit *test)\n+{\n+\tstatic const u8 udp_sport[] = {\n+\t\tWLAN_EID_TCLAS, 8,\n+\t\t255,\n+\t\t10, 0, 17,\t\t/* type, instance, protocol */\n+\t\t0x13, 0x88,\t\t/* value: source port 5000 */\n+\t\t0xff, 0xff,\t\t/* mask */\n+\t};\n+\tstatic const u8 wrong_sport[] = {\n+\t\tWLAN_EID_TCLAS, 8,\n+\t\t255,\n+\t\t10, 0, 17,\n+\t\t0x00, 0x35,\n+\t\t0xff, 0xff,\n+\t};\n+\tstruct cfg80211_scs_desc *list[1];\n+\tstruct cfg80211_scs_verdict verdict;\n+\tstruct cfg80211_flow_info info;\n+\n+\tKUNIT_ASSERT_TRUE(test,\n+\t\t\t cfg80211_flow_parse(TEST_SKB(test, udp_frame),\n+\t\t\t\t\t \u0026info));\n+\n+\tlist[0] = test_scs_desc(test, 1, 5, udp_sport, sizeof(udp_sport));\n+\tKUNIT_EXPECT_EQ(test, list[0]-\u003etclas[0].type, 10);\n+\tKUNIT_EXPECT_EQ(test, list[0]-\u003etclas[0].filter.len, 2);\n+\n+\tcfg80211_scs_evaluate(list, 1, \u0026info, \u0026verdict);\n+\tKUNIT_EXPECT_TRUE(test, verdict.match);\n+\tKUNIT_EXPECT_EQ(test, verdict.up, 5);\n+\n+\tlist[0] = test_scs_desc(test, 2, 5, wrong_sport, sizeof(wrong_sport));\n+\tcfg80211_scs_evaluate(list, 1, \u0026info, \u0026verdict);\n+\tKUNIT_EXPECT_FALSE(test, verdict.match);\n+}\n+\n+static struct kunit_case scs_cases[] = {\n+\tKUNIT_CASE(flow_parse_fragment),\n+\tKUNIT_CASE(scs_granularity),\n+\tKUNIT_CASE(scs_filter),\n+\tKUNIT_CASE(flow_key_mirror),\n+\tKUNIT_CASE(flow_key_mirror_eth),\n+\tKUNIT_CASE(tclas_refused),\n+\tKUNIT_CASE(qos_characteristics),\n+\tKUNIT_CASE(scs_traffic_description),\n+\t{}\n+};\n+\n+static struct kunit_suite scs = {\n+\t.name = \"cfg80211-scs\",\n+\t.test_cases = scs_cases,\n+};\n+\n+kunit_test_suite(scs);\ndiff --git a/net/wireless/trace.h b/net/wireless/trace.h\nindex 8c2a91b85c396..81c0899cb212c 100644\n--- a/net/wireless/trace.h\n+++ b/net/wireless/trace.h\n@@ -2636,6 +2636,55 @@ TRACE_EVENT(rdev_set_ap_chanwidth,\n \t\t __entry-\u003elink_id)\n );\n \n+TRACE_EVENT(rdev_set_scs,\n+\tTP_PROTO(struct wiphy *wiphy, struct net_device *netdev,\n+\t\t const u8 *peer, u8 n_desc),\n+\tTP_ARGS(wiphy, netdev, peer, n_desc),\n+\tTP_STRUCT__entry(\n+\t\tWIPHY_ENTRY\n+\t\tNETDEV_ENTRY\n+\t\tMAC_ENTRY(peer)\n+\t\t__field(u8, n_desc)\n+\t),\n+\tTP_fast_assign(\n+\t\tWIPHY_ASSIGN;\n+\t\tNETDEV_ASSIGN;\n+\t\tMAC_ASSIGN(peer, peer);\n+\t\t__entry-\u003en_desc = n_desc;\n+\t),\n+\tTP_printk(WIPHY_PR_FMT \", \" NETDEV_PR_FMT \", %pM, %d descriptors\",\n+\t\t WIPHY_PR_ARG, NETDEV_PR_ARG, __entry-\u003epeer, __entry-\u003en_desc)\n+);\n+\n+TRACE_EVENT(rdev_set_mscs,\n+\tTP_PROTO(struct wiphy *wiphy, struct net_device *netdev,\n+\t\t const u8 *peer, const struct cfg80211_mscs_desc *desc),\n+\tTP_ARGS(wiphy, netdev, peer, desc),\n+\tTP_STRUCT__entry(\n+\t\tWIPHY_ENTRY\n+\t\tNETDEV_ENTRY\n+\t\tMAC_ENTRY(peer)\n+\t\t__field(u32, fields)\n+\t\t__field(u32, stream_timeout)\n+\t\t__field(u8, up_bitmap)\n+\t\t__field(u8, up_limit)\n+\t),\n+\tTP_fast_assign(\n+\t\tWIPHY_ASSIGN;\n+\t\tNETDEV_ASSIGN;\n+\t\tMAC_ASSIGN(peer, peer);\n+\t\t__entry-\u003efields = desc ? desc-\u003efields : 0;\n+\t\t__entry-\u003estream_timeout = desc ? desc-\u003estream_timeout : 0;\n+\t\t__entry-\u003eup_bitmap = desc ? desc-\u003eup_bitmap : 0;\n+\t\t__entry-\u003eup_limit = desc ? desc-\u003eup_limit : 0;\n+\t),\n+\tTP_printk(WIPHY_PR_FMT \", \" NETDEV_PR_FMT\n+\t\t \", %pM, fields %x, UP bitmap %x, UP limit %d, timeout %d\",\n+\t\t WIPHY_PR_ARG, NETDEV_PR_ARG, __entry-\u003epeer, __entry-\u003efields,\n+\t\t __entry-\u003eup_bitmap, __entry-\u003eup_limit,\n+\t\t __entry-\u003estream_timeout)\n+);\n+\n TRACE_EVENT(rdev_add_tx_ts,\n \tTP_PROTO(struct wiphy *wiphy, struct net_device *netdev,\n \t\t u8 tsid, const u8 *peer, u8 user_prio, u16 admitted_time),\n"
} |
| 2/1 | 2026/09/24 13:41 | agent | patch-evaluator |
2mModel:core Args: null Results: {
"EnableConfigs": [
"CFG80211",
"MAC80211",
"MAC80211_HWSIM",
"CFG80211_DEBUGFS",
"MAC80211_DEBUGFS",
"DEBUG_FS"
],
"FocusSymbols": [
"nl80211_set_scs",
"nl80211_set_mscs",
"nl80211_parse_scs_desc",
"ieee80211_set_scs",
"ieee80211_set_mscs",
"ieee80211_flow_classify",
"ieee80211_flow_learn",
"cfg80211_parse_tclas",
"cfg80211_parse_tclas_mask",
"cfg80211_flow_parse",
"cfg80211_flow_key_build",
"cfg80211_scs_evaluate",
"mac80211_hwsim_sta_set_scs"
],
"Reasoning": "The patch series implements support for Stream Classification Service (SCS) and Mirrored SCS (MSCS) across cfg80211, mac80211, and the virtual mac80211_hwsim driver. It introduces new user-accessible netlink commands (NL80211_CMD_SET_SCS and NL80211_CMD_SET_MSCS), TCLAS element parsing, flow key matching, rhashtable management, and packet classification in RX/TX fast paths. All of this code is directly reachable in standard virtualized environments (e.g. QEMU) using mac80211_hwsim and nl80211 netlink sockets, making it highly valuable for fuzzing.",
"WorthFuzzing": true
}Instruction: You are an expert Linux kernel maintainer and security engineer.
Your job is to review a provided patch series and evaluate whether it warrants fuzzing with syzkaller.
IMPORTANT: The changes have ALREADY been applied and committed as the HEAD commit in
your workspace. Do NOT rely on internal assumptions. You must actively use your code access
tools to inspect the actual source code, callers, and surrounding context.
================================================================================
1. CORE TRIAGE PHILOSOPHY
================================================================================
The goal of patch fuzzing is to discover crashes, regressions, exposed latent bugs,
and newly triggered assertions introduced by the patch series.
- REACHABILITY IS THE PRIMARY GATE:
Fuzzing can only discover bugs in code that can actually execute in standard virtualized
environments (GCE or QEMU, utilizing software-emulated devices like USB gadgets, netdev, tun/tap).
If the modified code is structurally unreachable (see Section 2), it MUST NOT be fuzzed,
regardless of whether it adds assertions or complex logic.
- DO NOT BLINDLY TRUST "NO FUNCTIONAL CHANGE" (NFCI) OR "REFACTORING" CLAIMS:
Patch authors routinely label changes as "cleanups", "refactorings", or state
"No functional change intended". Do NOT take these claims at face value.
Code refactorings that rearrange logic, introduce helper functions, or alter state management
in core subsystems frequently introduce subtle semantic shifts or uncover latent kernel bugs.
If reachable executable code is modified or refactored, it MUST be fuzzed.
- NEW OR MODIFIED ASSERTIONS IN REACHABLE CODE MUST BE FUZZED:
When a patch introduces or modifies runtime checks or assertions (e.g., WARN_ON*, VM_WARN_ON*,
BUG_ON*, lockdep_assert*) in reachable code paths, it enforces new or stricter invariants.
Even if the author believes the invariant always holds, fuzzing is essential to verify whether
an unusual sequence of operations can violate it.
================================================================================
2. WHEN TO RETURN WorthFuzzing=false (NEGATIVE CRITERIA)
================================================================================
Return WorthFuzzing=false ONLY IF all modified code falls strictly into one or more of these categories:
- Non-kernel and non-executable changes:
* Modifications to Documentation/, comments, or spelling fixes.
* User-space directories, self-tests, samples, or scripts (e.g., tools/, samples/, scripts/, usr/)
that do not affect the compiled kernel image (vmlinux) or kernel modules.
* Purely decorative logging (e.g., message strings in pr_err, printk, dev_info) or tracepoints
that do not alter control flow or data structures.
* Build system or Kconfig changes that do not alter compiled C logic.
- Structurally unreachable hardware:
* Vendor-specific PCIe switches, SmartNICs, or GPU drivers (e.g., mlxsw, pds_core, qed,
ionic, amdgpu) requiring physical ASIC/PCIe cards not emulated in standard QEMU.
- Unreachable execution paths:
* Driver teardown callbacks (.remove, .shutdown, pci_unregister_driver) executed only during
physical PCI hot-unplug or manual sysfs driver unbinding.
* Code paths exclusive to architectures other than the target architecture.
================================================================================
3. WHEN TO RETURN WorthFuzzing=true (POSITIVE CRITERIA)
================================================================================
Return WorthFuzzing=true whenever the patch touches reachable executable code, including:
- Core Subsystems:
* Any logic modifications in memory management (mm/), synchronization/locking (kernel/locking/),
BPF, scheduler, core networking, VFS, or syscall handling.
- Refactorings and Code Cleanups:
* Any restructuring of reachable data structures, helper abstractions, or algorithm flows.
- Runtime Assertions and Defensive Checks:
* Any introduction or alteration of assertions (WARN_ON*, VM_WARN_ON*, BUG_ON*, etc.) in reachable paths.
- Reachable Drivers and Protocols:
* Drivers accessible via virtual buses (virtio, USB gadget, loopback, netlink, binder, sockets, etc.).
================================================================================
4. EXTRACTING FocusSymbols (PREVENTING DILUTION)
================================================================================
When WorthFuzzing=true, you must extract specific kernel functions into FocusSymbols to guide the fuzzer:
- AVOID UBIQUITOUS LIFECYCLE HOT-PATHS:
Do NOT list generic, ubiquitous functions called by almost every program in the corpus
(including, but not limited to: general memory allocators and deallocators, page fault
and trap handlers, or core synchronization primitives; this is not an exhaustive list).
Listing ubiquitous functions causes the fuzzer to classify thousands of unrelated tests as "focused",
which severely dilutes fuzzing effort away from the actual changes.
- TARGET SPECIFIC FEATURE LOGIC AND ENTRYPOINTS:
List functions that specifically implement the logic being added or altered, or direct API entrypoints
for the subsystem feature under review.
- HANDLING STATIC INLINE FUNCTIONS IN HEADERS (.h):
Compiler-inlined static functions (such as static inlines in mm/*.h or include/linux/*.h) lack
distinct symbol addresses in vmlinux and cannot be targeted directly by symbol coverage filters.
If the changes are primarily in static inline helpers, identify non-static, feature-specific caller
functions in .c files that exercise them (avoiding ubiquitous lifecycle wrappers).
================================================================================
5. IDENTIFYING EnableConfigs
================================================================================
Identify any specific CONFIG_ options required to properly compile and reach the modified code:
- Inspect Kconfig files and #ifdef guards; do not make assumptions.
- Check "depends on" lines in Kconfig to include any non-standard parent subsystem configs needed.
- Strip any 'CONFIG_' prefix (e.g., return "NET_IPV4" instead of "CONFIG_NET_IPV4").
Prefer calling several tools at the same time to save round-trips.
Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.
Prompt: Target architecture: amd64
For your convenience, here is the diff of the changes:
commit 80a1526dde5ea12c2da885d6b6b0e2c4c75ac2a4
Author: syz-cluster <triage@syzkaller.com>
Date: Thu Sep 24 13:41:14 2026 +0000
syz-cluster: applied patch under review
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/init.c b/drivers/net/wireless/mediatek/mt76/mt7996/init.c
index fb635a0925840..ca161d644d728 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/init.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/init.c
@@ -526,6 +526,8 @@ mt7996_init_wiphy(struct ieee80211_hw *hw, struct mtk_wed_device *wed)
wiphy_ext_feature_set(wiphy, NL80211_EXT_FEATURE_CAN_REPLACE_PTK0);
wiphy_ext_feature_set(wiphy, NL80211_EXT_FEATURE_MU_MIMO_AIR_SNIFFER);
wiphy_ext_feature_set(wiphy, NL80211_EXT_FEATURE_SET_SCAN_DWELL);
+ wiphy_ext_feature_set(wiphy, NL80211_EXT_FEATURE_SCS);
+ wiphy_ext_feature_set(wiphy, NL80211_EXT_FEATURE_MSCS);
if (mt7996_eeprom_has_background_radar(dev) &&
(!mdev->dev->of_node ||
@@ -1580,6 +1582,7 @@ mt7996_init_eht_caps(struct mt7996_phy *phy, enum nl80211_band band,
eht_cap_elem->mac_cap_info[0] =
IEEE80211_EHT_MAC_CAP0_OM_CONTROL |
+ IEEE80211_EHT_MAC_CAP0_SCS_TRAFFIC_DESC |
u8_encode_bits(IEEE80211_EHT_MAC_CAP0_MAX_MPDU_LEN_11454,
IEEE80211_EHT_MAC_CAP0_MAX_MPDU_LEN_MASK);
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index e218856b0c454..6b2d24fe70f10 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -2051,6 +2051,69 @@ static void mt7996_sta_set_4addr(struct ieee80211_hw *hw,
mutex_unlock(&dev->mt76.mutex);
}
+/* SCS descriptors belong to the MLD, firmware streams to a link */
+static int mt7996_sta_scs_program(struct mt7996_dev *dev,
+ struct ieee80211_vif *vif,
+ struct ieee80211_sta *sta,
+ const struct cfg80211_scs_desc *desc,
+ bool del)
+{
+ struct mt7996_sta *msta = (struct mt7996_sta *)sta->drv_priv;
+ struct ieee80211_link_sta *link_sta;
+ unsigned int link_id;
+ bool any = false;
+ int ret = 0;
+
+ for_each_sta_active_link(vif, sta, link_sta, link_id) {
+ struct mt7996_sta_link *msta_link;
+
+ msta_link = mt7996_sta_link_protected(dev, msta, link_id);
+ if (!msta_link || !msta_link->wcid.sta)
+ continue;
+
+ any = true;
+ if (mt7996_mcu_set_scs(dev, msta_link->wcid.idx, desc, del))
+ ret = -EIO;
+ }
+
+ return any ? ret : -EIO;
+}
+
+static int mt7996_sta_set_scs(struct ieee80211_hw *hw,
+ struct ieee80211_vif *vif,
+ struct ieee80211_sta *sta,
+ struct cfg80211_scs_desc * const *desc,
+ struct cfg80211_scs_result *res, u8 n_desc)
+{
+ struct mt7996_dev *dev = mt7996_hw_dev(hw);
+ u8 i;
+
+ mutex_lock(&dev->mt76.mutex);
+
+ for (i = 0; i < n_desc; i++) {
+ const struct cfg80211_scs_desc *d = desc[i];
+ bool del = d->req_type == NL80211_SCS_REQ_REMOVE ||
+ !d->qos_char;
+
+ /* The firmware only supports uplink streams */
+ if (!del && ieee80211_qos_char_direction(d->qos_char) !=
+ IEEE80211_QOS_CHAR_DIR_UPLINK) {
+ res[i].status = WLAN_STATUS_REQUEST_DECLINED;
+ continue;
+ }
+
+ if (!mt7996_sta_scs_program(dev, vif, sta, d, del) || del)
+ continue;
+
+ mt7996_sta_scs_program(dev, vif, sta, d, true);
+ res[i].status = WLAN_STATUS_REQUEST_DECLINED;
+ }
+
+ mutex_unlock(&dev->mt76.mutex);
+
+ return 0;
+}
+
static void mt7996_sta_set_decap_offload(struct ieee80211_hw *hw,
struct ieee80211_vif *vif,
struct ieee80211_sta *sta,
@@ -2588,6 +2651,7 @@ const struct ieee80211_ops mt7996_ops = {
.set_coverage_class = mt7996_set_coverage_class,
.sta_statistics = mt7996_sta_statistics,
.sta_set_4addr = mt7996_sta_set_4addr,
+ .sta_set_scs = mt7996_sta_set_scs,
.sta_set_decap_offload = mt7996_sta_set_decap_offload,
.add_twt_setup = mt7996_mac_add_twt_setup,
.twt_teardown_request = mt7996_twt_teardown_request,
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
index cd7587ce5b8a6..fceffdeb8ca50 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
@@ -2616,6 +2616,45 @@ int mt7996_mcu_add_rate_ctrl(struct mt7996_dev *dev, struct mt7996_sta *msta,
return ret;
}
+int mt7996_mcu_set_scs(struct mt7996_dev *dev, u16 wlan_idx,
+ const struct cfg80211_scs_desc *desc, bool del)
+{
+#define MURU_QOS_FLAG_UPDATE 20
+#define MURU_QOS_FLAG_DELETE 21
+ struct {
+ u8 __rsv1[4];
+
+ __le16 tag;
+ __le16 len;
+ __le32 qos_flag;
+ __le16 wlan_idx;
+ u8 __rsv2[12];
+ u8 dir;
+ u8 __rsv3[4];
+ u8 scs_id;
+ u8 qos_ie[44];
+ } __packed req = {
+ .tag = cpu_to_le16(UNI_CMD_MURU_SET_QOS_CFG),
+ .len = cpu_to_le16(sizeof(req) - 4),
+ .wlan_idx = cpu_to_le16(wlan_idx),
+ .scs_id = desc->id,
+ };
+
+ if (del) {
+ req.qos_flag = cpu_to_le32(MURU_QOS_FLAG_DELETE);
+ } else {
+ if (desc->qos_char_len > sizeof(req.qos_ie))
+ return -EINVAL;
+
+ req.qos_flag = cpu_to_le32(MURU_QOS_FLAG_UPDATE);
+ req.dir = ieee80211_qos_char_direction(desc->qos_char);
+ memcpy(req.qos_ie, desc->qos_char, desc->qos_char_len);
+ }
+
+ return mt76_mcu_send_msg(&dev->mt76, MCU_WMWA_UNI_CMD(MURU), &req,
+ sizeof(req), true);
+}
+
static int
mt7996_mcu_add_group(struct mt7996_dev *dev, struct mt7996_vif_link *link,
struct mt76_wcid *wcid)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.h b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.h
index 74b70fb6da3da..efd474cf8da9c 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.h
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.h
@@ -990,6 +990,10 @@ enum {
UNI_EXT_EEPROM_ACCESS = 1,
};
+enum {
+ UNI_CMD_MURU_SET_QOS_CFG = 0xfe,
+};
+
enum {
UNI_VOW_DRR_CTRL,
UNI_VOW_RX_AT_AIRTIME_EN = 0x0b,
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h b/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
index 2a0cdb56f822e..ebd55d8da654f 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
@@ -754,6 +754,8 @@ int mt7996_mcu_set_fixed_rate_ctrl(struct mt7996_dev *dev,
void *data, u16 version);
int mt7996_mcu_set_fixed_field(struct mt7996_dev *dev, struct mt7996_sta *msta,
void *data, u8 link_id, u32 field);
+int mt7996_mcu_set_scs(struct mt7996_dev *dev, u16 wlan_idx,
+ const struct cfg80211_scs_desc *desc, bool del);
int mt7996_mcu_set_eeprom(struct mt7996_dev *dev);
int mt7996_mcu_get_eeprom(struct mt7996_dev *dev, u32 offset, u8 *buf, u32 buf_len,
enum mt7996_eeprom_mode mode);
diff --git a/drivers/net/wireless/virtual/mac80211_hwsim_i.h b/drivers/net/wireless/virtual/mac80211_hwsim_i.h
index 0f0f2ac6d80eb..7f1c85ea6813e 100644
--- a/drivers/net/wireless/virtual/mac80211_hwsim_i.h
+++ b/drivers/net/wireless/virtual/mac80211_hwsim_i.h
@@ -137,6 +137,9 @@ struct mac80211_hwsim_data {
/* RSSI in rx status of the receiver */
int rx_rssi;
+ /* Status code for a traffic description, 0 accepts */
+ u16 scs_status;
+
/* only used when pmsr capability is supplied */
struct cfg80211_pmsr_capabilities pmsr_capa;
struct cfg80211_pmsr_request *pmsr_request;
diff --git a/drivers/net/wireless/virtual/mac80211_hwsim_main.c b/drivers/net/wireless/virtual/mac80211_hwsim_main.c
index d567ed8d45a43..828dde53747a5 100644
--- a/drivers/net/wireless/virtual/mac80211_hwsim_main.c
+++ b/drivers/net/wireless/virtual/mac80211_hwsim_main.c
@@ -4294,6 +4294,24 @@ static int mac80211_hwsim_set_radar_background(struct ieee80211_hw *hw,
return 0;
}
+/* The status code for a traffic description is set via debugfs for testing */
+static int mac80211_hwsim_sta_set_scs(struct ieee80211_hw *hw,
+ struct ieee80211_vif *vif,
+ struct ieee80211_sta *sta,
+ struct cfg80211_scs_desc * const *desc,
+ struct cfg80211_scs_result *res,
+ u8 n_desc)
+{
+ struct mac80211_hwsim_data *data = hw->priv;
+ u8 i;
+
+ for (i = 0; i < n_desc; i++)
+ if (desc[i]->qos_char)
+ res[i].status = data->scs_status;
+
+ return 0;
+}
+
#ifdef CONFIG_MAC80211_DEBUGFS
#define HWSIM_DEBUGFS_OPS \
.link_add_debugfs = mac80211_hwsim_link_add_debugfs,
@@ -4318,6 +4336,7 @@ static int mac80211_hwsim_set_radar_background(struct ieee80211_hw *hw,
.link_sta_rc_update = mac80211_hwsim_sta_rc_update, \
.conf_tx = mac80211_hwsim_conf_tx, \
.get_survey = mac80211_hwsim_get_survey, \
+ .sta_set_scs = mac80211_hwsim_sta_set_scs, \
CFG80211_TESTMODE_CMD(mac80211_hwsim_testmode_cmd) \
.ampdu_action = mac80211_hwsim_ampdu_action, \
.flush = mac80211_hwsim_flush, \
@@ -5973,6 +5992,8 @@ static int mac80211_hwsim_new_radio(struct genl_info *info,
NL80211_EXT_FEATURE_EXT_KEY_ID);
wiphy_ext_feature_set(hw->wiphy,
NL80211_EXT_FEATURE_ASSOC_FRAME_ENCRYPTION);
+ wiphy_ext_feature_set(hw->wiphy, NL80211_EXT_FEATURE_SCS);
+ wiphy_ext_feature_set(hw->wiphy, NL80211_EXT_FEATURE_MSCS);
hw->wiphy->interface_modes = param->iftypes;
@@ -6162,6 +6183,8 @@ static int mac80211_hwsim_new_radio(struct genl_info *info,
&hwsim_fops_group);
debugfs_create_file("rx_rssi", 0666, data->debugfs, data,
&hwsim_fops_rx_rssi);
+ debugfs_create_u16("scs_status", 0600, data->debugfs,
+ &data->scs_status);
if (!data->use_chanctx)
debugfs_create_file("dfs_simulate_radar", 0222,
data->debugfs,
diff --git a/include/linux/ieee80211-eht.h b/include/linux/ieee80211-eht.h
index a699d108eee61..92fa7efdf156c 100644
--- a/include/linux/ieee80211-eht.h
+++ b/include/linux/ieee80211-eht.h
@@ -1385,4 +1385,129 @@ static inline u32 ieee80211_eml_trans_timeout_in_us(u16 eml_cap)
_data + ieee80211_mle_common_size(_data),\
_len - ieee80211_mle_common_size(_data))
+/**
+ * struct ieee80211_qos_char_elem - QoS Characteristics element
+ * @element_id: %WLAN_EID_EXTENSION
+ * @length: length of everything behind it
+ * @element_id_ext: %WLAN_EID_EXT_QOS_CHARACTERISTICS
+ * @control: control info, see %IEEE80211_QOS_CHAR_CTRL_*
+ * @min_service_interval: minimum service period interval, in microseconds
+ * @max_service_interval: maximum service period interval, in microseconds
+ * @min_data_rate: minimum data rate at the MAC SAP, in kilobits per second
+ * @delay_bound: targeted transport time of an MSDU, in microseconds
+ * @variable: the optional parameters that the presence bitmap names, in the
+ * order of the %IEEE80211_QOS_CHAR_PRES_* bits
+ *
+ * The whole element, header included, because a device takes it unchanged.
+ */
+struct ieee80211_qos_char_elem {
+ u8 element_id;
+ u8 length;
+ u8 element_id_ext;
+ __le32 control;
+ __le32 min_service_interval;
+ __le32 max_service_interval;
+ u8 min_data_rate[3];
+ u8 delay_bound[3];
+ u8 variable[];
+} __packed;
+
+#define IEEE80211_QOS_CHAR_CTRL_DIRECTION GENMASK(1, 0)
+#define IEEE80211_QOS_CHAR_CTRL_PRESENCE GENMASK(24, 9)
+
+#define IEEE80211_QOS_CHAR_DIR_UPLINK 0
+#define IEEE80211_QOS_CHAR_DIR_DOWNLINK 1
+#define IEEE80211_QOS_CHAR_DIR_DIRECT 2
+
+#define IEEE80211_QOS_CHAR_PRES_MAX_MSDU_SIZE BIT(0)
+#define IEEE80211_QOS_CHAR_PRES_SERVICE_START_TIME BIT(1)
+#define IEEE80211_QOS_CHAR_PRES_SERVICE_START_LINK_ID BIT(2)
+#define IEEE80211_QOS_CHAR_PRES_MEAN_DATA_RATE BIT(3)
+#define IEEE80211_QOS_CHAR_PRES_BURST_SIZE BIT(4)
+#define IEEE80211_QOS_CHAR_PRES_MSDU_LIFETIME BIT(5)
+#define IEEE80211_QOS_CHAR_PRES_MSDU_DELIVERY_INFO BIT(6)
+#define IEEE80211_QOS_CHAR_PRES_MEDIUM_TIME BIT(7)
+
+/**
+ * ieee80211_qos_char_presence - QoS Characteristics presence bitmap
+ * @qc: the element
+ * Return: the Presence Bitmap Of Additional Parameters subfield, a bitmap of
+ * %IEEE80211_QOS_CHAR_PRES_*
+ */
+static inline u16
+ieee80211_qos_char_presence(const struct ieee80211_qos_char_elem *qc)
+{
+ return u32_get_bits(le32_to_cpu(qc->control),
+ IEEE80211_QOS_CHAR_CTRL_PRESENCE);
+}
+
+/**
+ * ieee80211_qos_char_direction - QoS Characteristics direction
+ * @qc: the element
+ * Return: %IEEE80211_QOS_CHAR_DIR_UPLINK, _DOWNLINK or _DIRECT
+ */
+static inline u8
+ieee80211_qos_char_direction(const struct ieee80211_qos_char_elem *qc)
+{
+ return u32_get_bits(le32_to_cpu(qc->control),
+ IEEE80211_QOS_CHAR_CTRL_DIRECTION);
+}
+
+/**
+ * ieee80211_qos_char_size_ok - check a QoS Characteristics element
+ * @data: candidate octets, from the Element ID
+ * @len: length of @data
+ *
+ * Call this before the other accessors, which assume a valid element.
+ *
+ * Return: %true if @data holds a QoS Characteristics element whose length
+ * matches its presence bitmap
+ */
+static inline bool ieee80211_qos_char_size_ok(const u8 *data, size_t len)
+{
+ const struct ieee80211_qos_char_elem *qc = (const void *)data;
+ size_t needed = sizeof(*qc);
+ u16 present;
+
+ if (len < needed)
+ return false;
+
+ if (qc->element_id != WLAN_EID_EXTENSION ||
+ qc->element_id_ext != WLAN_EID_EXT_QOS_CHARACTERISTICS ||
+ qc->length != len - 2)
+ return false;
+
+ present = ieee80211_qos_char_presence(qc);
+
+ if (present & ~GENMASK(7, 0))
+ return false;
+
+ if (ieee80211_qos_char_direction(qc) > IEEE80211_QOS_CHAR_DIR_DIRECT)
+ return false;
+
+ /* The LinkID refers to the Service Start Time, which must be present */
+ if (present & IEEE80211_QOS_CHAR_PRES_SERVICE_START_LINK_ID &&
+ !(present & IEEE80211_QOS_CHAR_PRES_SERVICE_START_TIME))
+ return false;
+
+ if (present & IEEE80211_QOS_CHAR_PRES_MAX_MSDU_SIZE)
+ needed += 2;
+ if (present & IEEE80211_QOS_CHAR_PRES_SERVICE_START_TIME)
+ needed += 4;
+ if (present & IEEE80211_QOS_CHAR_PRES_SERVICE_START_LINK_ID)
+ needed += 1;
+ if (present & IEEE80211_QOS_CHAR_PRES_MEAN_DATA_RATE)
+ needed += 3;
+ if (present & IEEE80211_QOS_CHAR_PRES_BURST_SIZE)
+ needed += 4;
+ if (present & IEEE80211_QOS_CHAR_PRES_MSDU_LIFETIME)
+ needed += 2;
+ if (present & IEEE80211_QOS_CHAR_PRES_MSDU_DELIVERY_INFO)
+ needed += 1;
+ if (present & IEEE80211_QOS_CHAR_PRES_MEDIUM_TIME)
+ needed += 2;
+
+ return len == needed;
+}
+
#endif /* LINUX_IEEE80211_EHT_H */
diff --git a/include/linux/ieee80211.h b/include/linux/ieee80211.h
index 7fbc51f873ff1..4990db36cd540 100644
--- a/include/linux/ieee80211.h
+++ b/include/linux/ieee80211.h
@@ -1501,6 +1501,9 @@ enum ieee80211_statuscode {
WLAN_STATUS_NO_DIRECT_LINK = 48,
WLAN_STATUS_STA_NOT_PRESENT = 49,
WLAN_STATUS_STA_NOT_QSTA = 50,
+ WLAN_STATUS_REQUESTED_TCLAS_NOT_SUPPORTED_BY_AP = 56,
+ WLAN_STATUS_INSUFFICIENT_TCLAS_PROCESSING_RESOURCES = 57,
+ WLAN_STATUS_TCLAS_PROCESSING_TERMINATED = 97,
/* 802.11s */
WLAN_STATUS_ANTI_CLOG_REQUIRED = 76,
WLAN_STATUS_FCG_NOT_SUPP = 78,
@@ -1821,10 +1824,12 @@ enum ieee80211_eid_ext {
WLAN_EID_EXT_SHORT_SSID_LIST = 58,
WLAN_EID_EXT_HE_6GHZ_CAPA = 59,
WLAN_EID_EXT_UL_MU_POWER_CAPA = 60,
+ WLAN_EID_EXT_TCLAS_MASK = 89,
WLAN_EID_EXT_EHT_OPERATION = 106,
WLAN_EID_EXT_EHT_MULTI_LINK = 107,
WLAN_EID_EXT_EHT_CAPABILITY = 108,
WLAN_EID_EXT_TID_TO_LINK_MAPPING = 109,
+ WLAN_EID_EXT_QOS_CHARACTERISTICS = 113,
WLAN_EID_EXT_BANDWIDTH_INDICATION = 135,
WLAN_EID_EXT_KNOWN_STA_IDENTIFCATION = 136,
WLAN_EID_EXT_NON_AP_STA_REG_CON = 137,
diff --git a/include/net/cfg80211.h b/include/net/cfg80211.h
index 9aa7d2d4a1841..58b28c53a86b6 100644
--- a/include/net/cfg80211.h
+++ b/include/net/cfg80211.h
@@ -4069,6 +4069,249 @@ struct cfg80211_qos_map {
struct cfg80211_dscp_range up[8];
};
+/**
+ * enum cfg80211_tclas_processing - how the TCLAS elements relate
+ *
+ * @CFG80211_TCLAS_PROCESSING_ALL: an MSDU has to match every element
+ * @CFG80211_TCLAS_PROCESSING_ANY: one matching element is enough
+ * @CFG80211_TCLAS_PROCESSING_DEFAULT: matches every MSDU that no other
+ * descriptor matches, and carries no element
+ * @CFG80211_TCLAS_PROCESSING_ABSENT: the descriptor carries no TCLAS
+ * Processing element. Not a value of the Processing field.
+ */
+enum cfg80211_tclas_processing {
+ CFG80211_TCLAS_PROCESSING_ALL = 0,
+ CFG80211_TCLAS_PROCESSING_ANY = 1,
+ CFG80211_TCLAS_PROCESSING_DEFAULT = 2,
+
+ CFG80211_TCLAS_PROCESSING_ABSENT = 0xff,
+};
+
+/**
+ * enum cfg80211_flow_field - classifier parameter of one MSDU
+ *
+ * A set of these is held as a bitmap of BIT(field) in a u32.
+ *
+ * @CFG80211_FLOW_F_ETH_SA: Ethernet source address
+ * @CFG80211_FLOW_F_ETH_DA: Ethernet destination address
+ * @CFG80211_FLOW_F_ETH_TYPE: ethertype
+ * @CFG80211_FLOW_F_VLAN_PCP: IEEE 802.1Q priority code point
+ * @CFG80211_FLOW_F_VLAN_DEI: IEEE 802.1Q drop eligibility indicator
+ * @CFG80211_FLOW_F_VLAN_VID: IEEE 802.1Q VLAN identifier
+ * @CFG80211_FLOW_F_IP_VERSION: IP version
+ * @CFG80211_FLOW_F_IP_SRC: IP source address
+ * @CFG80211_FLOW_F_IP_DST: IP destination address
+ * @CFG80211_FLOW_F_SRC_PORT: layer 4 source port
+ * @CFG80211_FLOW_F_DST_PORT: layer 4 destination port
+ * @CFG80211_FLOW_F_DSCP: differentiated services code point
+ * @CFG80211_FLOW_F_PROTO: IPv4 protocol or IPv6 next header
+ * @CFG80211_FLOW_F_FLOW_LABEL: IPv6 flow label
+ * @NUM_CFG80211_FLOW_FIELDS: number of fields
+ */
+enum cfg80211_flow_field {
+ CFG80211_FLOW_F_ETH_SA,
+ CFG80211_FLOW_F_ETH_DA,
+ CFG80211_FLOW_F_ETH_TYPE,
+ CFG80211_FLOW_F_VLAN_PCP,
+ CFG80211_FLOW_F_VLAN_DEI,
+ CFG80211_FLOW_F_VLAN_VID,
+ CFG80211_FLOW_F_IP_VERSION,
+ CFG80211_FLOW_F_IP_SRC,
+ CFG80211_FLOW_F_IP_DST,
+ CFG80211_FLOW_F_SRC_PORT,
+ CFG80211_FLOW_F_DST_PORT,
+ CFG80211_FLOW_F_DSCP,
+ CFG80211_FLOW_F_PROTO,
+ CFG80211_FLOW_F_FLOW_LABEL,
+
+ NUM_CFG80211_FLOW_FIELDS,
+};
+
+/**
+ * struct cfg80211_flow_key - masked classifier parameters of one MSDU
+ *
+ * Unselected fields are zero. The whole structure is hashed and compared as a
+ * byte string, so it must contain no undefined octet.
+ *
+ * @src: IP source address, IPv4 in the first four octets
+ * @dst: IP destination address, IPv4 in the first four octets
+ * @flow_label: IPv6 flow label
+ * @sa: Ethernet source address
+ * @da: Ethernet destination address
+ * @eth_type: ethertype
+ * @vlan_tci: IEEE 802.1Q tag control information
+ * @src_port: layer 4 source port
+ * @dst_port: layer 4 destination port
+ * @ip_version: IP version, which keeps IPv4 and IPv6 keys apart
+ * @dscp: differentiated services code point, in the six LSBs
+ * @proto: IPv4 protocol or IPv6 next header
+ * @pad: must be zero
+ */
+struct cfg80211_flow_key {
+ struct in6_addr src;
+ struct in6_addr dst;
+ __be32 flow_label;
+ u8 sa[ETH_ALEN];
+ u8 da[ETH_ALEN];
+ __be16 eth_type;
+ __be16 vlan_tci;
+ __be16 src_port;
+ __be16 dst_port;
+ u8 ip_version;
+ u8 dscp;
+ u8 proto;
+ u8 pad;
+};
+
+/* Longest Filter Value and Filter Mask a filter classifier may carry */
+#define CFG80211_TCLAS_FILTER_LEN 28
+
+/**
+ * struct cfg80211_tclas_filter - octet filter on a protocol payload
+ *
+ * @protocol: IPv4 protocol or IPv6 next header value that locates the
+ * payload to compare
+ * @instance: which occurrence of @protocol to use, counted from zero
+ * @len: length of @value and @mask
+ * @value: octets to compare against the payload
+ * @mask: the bits of @value that are compared
+ */
+struct cfg80211_tclas_filter {
+ u8 protocol;
+ u8 instance;
+ u8 len;
+ u8 value[CFG80211_TCLAS_FILTER_LEN];
+ u8 mask[CFG80211_TCLAS_FILTER_LEN];
+};
+
+/**
+ * struct cfg80211_tclas - parsed TCLAS element
+ *
+ * Only the classifier types the kernel can evaluate are represented, see
+ * cfg80211_parse_tclas().
+ *
+ * @type: classifier type
+ * @fields: the parameters the classifier mask selects, as a bitmap of
+ * &enum cfg80211_flow_field. Zero for a filter classifier, which
+ * selects no field of its own.
+ * @key: the required values, with unselected fields zero, so that a match is
+ * a comparison of two keys
+ * @filter: the octet filter, when @type is a filter classifier
+ */
+struct cfg80211_tclas {
+ u8 type;
+ u32 fields;
+ union {
+ struct cfg80211_flow_key key;
+ struct cfg80211_tclas_filter filter;
+ };
+};
+
+/**
+ * struct cfg80211_flow_info - classifier parameters of one MSDU
+ *
+ * @key: every parameter the source carries, with nothing masked out
+ * @present: bitmap of &enum cfg80211_flow_field, the parameters the source
+ * actually carries. A classifier that names an absent parameter cannot
+ * match.
+ * @skb: the frame the parameters came from, or %NULL when the caller
+ * describes a flow rather than a frame. A filter classifier reads the
+ * frame again and cannot be evaluated without it.
+ * @l3_off: offset of the IP header in @skb
+ */
+struct cfg80211_flow_info {
+ struct cfg80211_flow_key key;
+ u32 present;
+ const struct sk_buff *skb;
+ u16 l3_off;
+};
+
+bool cfg80211_flow_parse(struct sk_buff *skb, struct cfg80211_flow_info *info);
+
+/**
+ * enum cfg80211_flow_dir - direction of a built flow key
+ *
+ * @CFG80211_FLOW_AS_IS: the key describes the frame that was parsed
+ * @CFG80211_FLOW_MIRRORED: the key describes the reverse direction, with the
+ * addresses and the ports swapped
+ */
+enum cfg80211_flow_dir {
+ CFG80211_FLOW_AS_IS,
+ CFG80211_FLOW_MIRRORED,
+};
+
+bool cfg80211_flow_key_build(const struct cfg80211_flow_info *info, u32 fields,
+ enum cfg80211_flow_dir dir,
+ struct cfg80211_flow_key *key);
+
+/**
+ * struct cfg80211_scs_desc - one SCS descriptor
+ *
+ * @req_type: add, remove or change
+ * @tclas_processing: how the elements of @tclas relate
+ * @qos_char: the whole QoS Characteristics element, or %NULL. Set only
+ * after ieee80211_qos_char_size_ok() passed, so the accessors in
+ * <linux/ieee80211-eht.h> may read it.
+ * @id: SCSID, 1 to 255
+ * @up: user priority to assign to a matching MSDU
+ * @qos_char_len: length of @qos_char
+ * @n_tclas: number of entries in @tclas
+ * @tclas: the classifier
+ */
+struct cfg80211_scs_desc {
+ enum nl80211_scs_req_type req_type;
+ enum cfg80211_tclas_processing tclas_processing;
+ const struct ieee80211_qos_char_elem *qos_char;
+ u8 id;
+ u8 up;
+ u16 qos_char_len;
+ u8 n_tclas;
+ struct cfg80211_tclas tclas[] __counted_by(n_tclas);
+};
+
+/**
+ * struct cfg80211_scs_result - result for one SCS descriptor
+ *
+ * @status: IEEE status code
+ */
+struct cfg80211_scs_result {
+ u16 status;
+};
+
+/**
+ * struct cfg80211_mscs_desc - the MSCS of one peer
+ *
+ * @req_type: add, remove or change
+ * @up_bitmap: user priorities that the AP learns from, one bit each
+ * @up_limit: ceiling for the assigned user priority, 0 to 7
+ * @stream_timeout: minimum lifetime of a learned value, in TUs
+ * @fields: classifier parameters, a bitmap of &enum cfg80211_flow_field
+ */
+struct cfg80211_mscs_desc {
+ enum nl80211_scs_req_type req_type;
+ u8 up_bitmap;
+ u8 up_limit;
+ u32 stream_timeout;
+ u32 fields;
+};
+
+/**
+ * struct cfg80211_scs_verdict - result of an SCS evaluation
+ *
+ * @match: an SCS descriptor matched the MSDU
+ * @scsid: identifier of the matching descriptor
+ * @up: user priority to assign
+ */
+struct cfg80211_scs_verdict {
+ bool match;
+ u8 scsid;
+ u8 up;
+};
+
+void cfg80211_scs_evaluate(struct cfg80211_scs_desc * const *desc, u8 n_desc,
+ const struct cfg80211_flow_info *info,
+ struct cfg80211_scs_verdict *verdict);
+
/**
* DOC: Neighbor Awareness Networking (NAN)
*
@@ -5154,6 +5397,13 @@ struct mgmt_frame_regs {
*
* @set_qos_map: Set QoS mapping information to the driver
*
+ * @set_scs: Add, change or remove SCS descriptors of one peer. Userspace
+ * handles the action frame exchange. Each descriptor is answered with
+ * a status code in the result entry of the same index. A declined add
+ * or change leaves the previous classification of that SCSID in force.
+ * A negative return fails the request as a whole.
+ * @set_mscs: Install, change or remove the MSCS of one peer.
+ *
* @set_ap_chanwidth: Set the AP (including P2P GO) mode channel width for the
* given interface This is used e.g. for dynamic HT 20/40 MHz channel width
* changes during the lifetime of the BSS.
@@ -5558,6 +5808,13 @@ struct cfg80211_ops {
struct net_device *dev,
struct cfg80211_qos_map *qos_map);
+ int (*set_scs)(struct wiphy *wiphy, struct net_device *dev,
+ const u8 *peer,
+ struct cfg80211_scs_desc * const *desc,
+ struct cfg80211_scs_result *res, u8 n_desc);
+ int (*set_mscs)(struct wiphy *wiphy, struct net_device *dev,
+ const u8 *peer, struct cfg80211_mscs_desc *desc);
+
int (*set_ap_chanwidth)(struct wiphy *wiphy, struct net_device *dev,
unsigned int link_id,
struct cfg80211_chan_def *chandef);
diff --git a/include/net/mac80211.h b/include/net/mac80211.h
index d6f3c58559101..c65ebbb3265c3 100644
--- a/include/net/mac80211.h
+++ b/include/net/mac80211.h
@@ -5082,6 +5082,34 @@ struct ieee80211_ops {
struct cfg80211_pmsr_request *request);
void (*abort_pmsr)(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
struct cfg80211_pmsr_request *request);
+ /**
+ * @sta_set_scs:
+ * Called with the SCS descriptors that a station requested, after
+ * cfg80211 validated them. Optional, only reached when the wiphy
+ * advertises %NL80211_EXT_FEATURE_SCS. Without it, every descriptor
+ * with a QoS Characteristics element is declined.
+ *
+ * - The results arrive zeroed, which means accepted. Write only
+ * refusals, in the result entry of the same index.
+ * - Decline a descriptor only when its traffic description cannot
+ * be served. mac80211 classifies frames in software either way.
+ * - A declined add or change keeps the previous descriptor of that
+ * SCSID. An accepted one without a traffic description replaces
+ * the device state of that SCSID.
+ * - A removal only drops driver state, the result is ignored. A
+ * station going away drops its rules without a call.
+ * - After a hardware restart every stored rule is passed again as
+ * an add, one per call, and the result is ignored.
+ * - A negative return fails the whole request and leaves the rule
+ * set unchanged, so the driver must leave the device as it was.
+ * - The descriptors belong to the MLD, not to one link.
+ *
+ * Called with the wiphy mutex held. This callback may sleep.
+ */
+ int (*sta_set_scs)(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
+ struct ieee80211_sta *sta,
+ struct cfg80211_scs_desc * const *desc,
+ struct cfg80211_scs_result *res, u8 n_desc);
int (*set_tid_config)(struct ieee80211_hw *hw,
struct ieee80211_vif *vif,
struct ieee80211_sta *sta,
diff --git a/include/uapi/linux/nl80211.h b/include/uapi/linux/nl80211.h
index 9a2ccb8d66b86..ecb11739c5b7d 100644
--- a/include/uapi/linux/nl80211.h
+++ b/include/uapi/linux/nl80211.h
@@ -1427,6 +1427,18 @@
* @NL80211_CMD_STOP_PD: Stop the PD operation, identified by
* its %NL80211_ATTR_WDEV interface.
*
+ * @NL80211_CMD_SET_SCS: Add, change or remove stream classification service
+ * descriptors of the peer given by %NL80211_ATTR_MAC, as requested in an
+ * SCS Request frame. The descriptors are in
+ * %NL80211_ATTR_SCS_DESCRIPTORS. The reply repeats the attribute with
+ * the identifier and the status code of each descriptor. A declined
+ * descriptor does not make the command fail.
+ * @NL80211_CMD_SET_MSCS: Install, change or remove the mirrored stream
+ * classification service of the peer given by %NL80211_ATTR_MAC, as
+ * requested in an MSCS Request frame or a (Re)Association Request. The
+ * parameters are in %NL80211_ATTR_MSCS_DESCRIPTOR. The reply carries
+ * the status code in %NL80211_ATTR_STATUS_CODE.
+ *
* @NL80211_CMD_MAX: highest used command number
* @__NL80211_CMD_AFTER_LAST: internal use
*/
@@ -1705,6 +1717,9 @@ enum nl80211_commands {
NL80211_CMD_START_PD,
NL80211_CMD_STOP_PD,
+ NL80211_CMD_SET_SCS,
+ NL80211_CMD_SET_MSCS,
+
/* add new commands above here */
/* used to define NL80211_CMD_MAX below */
@@ -3190,6 +3205,12 @@ enum nl80211_commands {
* known station to transmit a frame. This is relevant to know whether
* MLD address translation happened or to disable it when sending a frame.
*
+ * @NL80211_ATTR_SCS_DESCRIPTORS: Nested array of SCS descriptors, each one a
+ * nested set of &enum nl80211_scs_desc_attr attributes. Used with
+ * %NL80211_CMD_SET_SCS in both directions.
+ * @NL80211_ATTR_MSCS_DESCRIPTOR: Nested set of &enum nl80211_mscs_desc_attr
+ * attributes. Used with %NL80211_CMD_SET_MSCS.
+ *
* @NUM_NL80211_ATTR: total number of nl80211_attrs available
* @NL80211_ATTR_MAX: highest attribute number currently defined
* @__NL80211_ATTR_AFTER_LAST: internal use
@@ -3792,6 +3813,9 @@ enum nl80211_attrs {
NL80211_ATTR_FRAME_NO_STA,
+ NL80211_ATTR_SCS_DESCRIPTORS,
+ NL80211_ATTR_MSCS_DESCRIPTOR,
+
/* add attributes here, update the policy in nl80211.c */
__NL80211_ATTR_AFTER_LAST,
@@ -6077,6 +6101,87 @@ enum nl80211_tx_power_setting {
NL80211_TX_POWER_FIXED,
};
+/**
+ * enum nl80211_scs_req_type - SCS or MSCS request type
+ *
+ * The values match the Request Type field of the descriptor on the air.
+ *
+ * @NL80211_SCS_REQ_ADD: create the stream, replacing an active one of the
+ * same identifier
+ * @NL80211_SCS_REQ_REMOVE: terminate the stream
+ * @NL80211_SCS_REQ_CHANGE: modify an active stream, leaving it in force if
+ * the request is declined
+ */
+enum nl80211_scs_req_type {
+ NL80211_SCS_REQ_ADD,
+ NL80211_SCS_REQ_REMOVE,
+ NL80211_SCS_REQ_CHANGE,
+};
+
+/**
+ * enum nl80211_scs_desc_attr - one SCS descriptor
+ *
+ * @__NL80211_SCS_DESC_ATTR_INVALID: invalid
+ * @NL80211_SCS_DESC_ATTR_ID: SCSID (u8, 1 to 255)
+ * @NL80211_SCS_DESC_ATTR_REQ_TYPE: request type (u8, see
+ * &enum nl80211_scs_req_type)
+ * @NL80211_SCS_DESC_ATTR_UP: user priority to assign to a matching MSDU
+ * (u8, 0 to 7), from the Intra-Access Category Priority element.
+ * Required for a downlink descriptor or one without
+ * %NL80211_SCS_DESC_ATTR_QOS_CHAR, and rejected otherwise.
+ * @NL80211_SCS_DESC_ATTR_TCLAS: one or more TCLAS elements and at most one
+ * TCLAS Processing element (binary), as received over the air
+ * @NL80211_SCS_DESC_ATTR_QOS_CHAR: the whole QoS Characteristics element
+ * (binary), header included, as received over the air
+ * @NL80211_SCS_DESC_ATTR_STATUS: IEEE status code (u16), kernel to userspace
+ *
+ * @__NL80211_SCS_DESC_ATTR_AFTER_LAST: internal use
+ * @NL80211_SCS_DESC_ATTR_MAX: highest attribute
+ */
+enum nl80211_scs_desc_attr {
+ __NL80211_SCS_DESC_ATTR_INVALID,
+ NL80211_SCS_DESC_ATTR_ID,
+ NL80211_SCS_DESC_ATTR_REQ_TYPE,
+ NL80211_SCS_DESC_ATTR_UP,
+ NL80211_SCS_DESC_ATTR_TCLAS,
+ NL80211_SCS_DESC_ATTR_QOS_CHAR,
+ NL80211_SCS_DESC_ATTR_STATUS,
+
+ __NL80211_SCS_DESC_ATTR_AFTER_LAST,
+ NL80211_SCS_DESC_ATTR_MAX = __NL80211_SCS_DESC_ATTR_AFTER_LAST - 1
+};
+
+/**
+ * enum nl80211_mscs_desc_attr - the MSCS of one peer
+ *
+ * @__NL80211_MSCS_DESC_ATTR_INVALID: invalid
+ * @NL80211_MSCS_DESC_ATTR_REQ_TYPE: request type (u8, see
+ * &enum nl80211_scs_req_type)
+ * @NL80211_MSCS_DESC_ATTR_UP_BITMAP: user priorities that the AP learns
+ * from, one bit each (u8)
+ * @NL80211_MSCS_DESC_ATTR_UP_LIMIT: ceiling for the assigned user priority
+ * (u8, 0 to 7)
+ * @NL80211_MSCS_DESC_ATTR_STREAM_TIMEOUT: minimum lifetime of a learned
+ * value, in TUs (u32)
+ * @NL80211_MSCS_DESC_ATTR_TCLAS_MASK: one or more TCLAS Mask elements
+ * (binary), as received over the air. Required for add and change,
+ * rejected for remove.
+ *
+ * @__NL80211_MSCS_DESC_ATTR_AFTER_LAST: internal use
+ * @NL80211_MSCS_DESC_ATTR_MAX: highest attribute
+ */
+enum nl80211_mscs_desc_attr {
+ __NL80211_MSCS_DESC_ATTR_INVALID,
+ NL80211_MSCS_DESC_ATTR_REQ_TYPE,
+ NL80211_MSCS_DESC_ATTR_UP_BITMAP,
+ NL80211_MSCS_DESC_ATTR_UP_LIMIT,
+ NL80211_MSCS_DESC_ATTR_STREAM_TIMEOUT,
+ NL80211_MSCS_DESC_ATTR_TCLAS_MASK,
+
+ __NL80211_MSCS_DESC_ATTR_AFTER_LAST,
+ NL80211_MSCS_DESC_ATTR_MAX = __NL80211_MSCS_DESC_ATTR_AFTER_LAST - 1
+};
+
/**
* enum nl80211_tid_config - TID config state
* @NL80211_TID_CONFIG_ENABLE: Enable config for the TID
@@ -7125,6 +7230,10 @@ enum nl80211_feature_flags {
* @NL80211_EXT_FEATURE_FAST_ROAM_OFFLOAD: Driver supports fast roaming
* offload in station mode, including Fast Transition or Opportunistic
* Key Caching.
+ * @NL80211_EXT_FEATURE_SCS: Driver supports the stream classification
+ * service, configured with %NL80211_CMD_SET_SCS.
+ * @NL80211_EXT_FEATURE_MSCS: Driver supports the mirrored stream
+ * classification service, configured with %NL80211_CMD_SET_MSCS.
*
* @NUM_NL80211_EXT_FEATURES: number of extended features.
* @MAX_NL80211_EXT_FEATURES: highest extended feature index.
@@ -7209,6 +7318,8 @@ enum nl80211_ext_feature_index {
NL80211_EXT_FEATURE_SET_KEY_LTF_SEED,
NL80211_EXT_FEATURE_PROBE_AP,
NL80211_EXT_FEATURE_FAST_ROAM_OFFLOAD,
+ NL80211_EXT_FEATURE_SCS,
+ NL80211_EXT_FEATURE_MSCS,
/* add new features before the definition below */
NUM_NL80211_EXT_FEATURES,
diff --git a/net/mac80211/Makefile b/net/mac80211/Makefile
index 36f1e3e2222b4..8f8729e44c5f1 100644
--- a/net/mac80211/Makefile
+++ b/net/mac80211/Makefile
@@ -35,7 +35,8 @@ mac80211-y := \
tdls.o \
ocb.o \
airtime.o \
- eht.o uhr.o nan.o
+ eht.o uhr.o nan.o \
+ scs.o
mac80211-$(CONFIG_MAC80211_LEDS) += led.o
mac80211-$(CONFIG_MAC80211_DEBUGFS) += \
diff --git a/net/mac80211/cfg.c b/net/mac80211/cfg.c
index a1753335eb9d0..04c5947718c3c 100644
--- a/net/mac80211/cfg.c
+++ b/net/mac80211/cfg.c
@@ -6073,6 +6073,8 @@ const struct cfg80211_ops mac80211_config_ops = {
.end_cac = ieee80211_end_cac,
.channel_switch = ieee80211_channel_switch,
.set_qos_map = ieee80211_set_qos_map,
+ .set_scs = ieee80211_set_scs,
+ .set_mscs = ieee80211_set_mscs,
.set_ap_chanwidth = ieee80211_set_ap_chanwidth,
.add_tx_ts = ieee80211_add_tx_ts,
.del_tx_ts = ieee80211_del_tx_ts,
diff --git a/net/mac80211/debugfs_sta.c b/net/mac80211/debugfs_sta.c
index ef75255d47d5a..bcf4961af24f4 100644
--- a/net/mac80211/debugfs_sta.c
+++ b/net/mac80211/debugfs_sta.c
@@ -10,6 +10,7 @@
#include <linux/debugfs.h>
#include <linux/ieee80211.h>
+#include <linux/if_vlan.h>
#include "ieee80211_i.h"
#include "debugfs.h"
#include "debugfs_sta.h"
@@ -131,6 +132,243 @@ static ssize_t sta_last_seq_ctrl_read(struct file *file, char __user *userbuf,
}
STA_OPS(last_seq_ctrl);
+static void sta_flow_key_show(struct seq_file *s,
+ const struct cfg80211_flow_key *key, u32 fields)
+{
+ u16 tci = be16_to_cpu(key->vlan_tci);
+
+ if (fields & BIT(CFG80211_FLOW_F_ETH_SA))
+ seq_printf(s, " sa=%pM", key->sa);
+ if (fields & BIT(CFG80211_FLOW_F_ETH_DA))
+ seq_printf(s, " da=%pM", key->da);
+ if (fields & BIT(CFG80211_FLOW_F_ETH_TYPE))
+ seq_printf(s, " ethertype=0x%04x", be16_to_cpu(key->eth_type));
+
+ if (fields & BIT(CFG80211_FLOW_F_VLAN_PCP))
+ seq_printf(s, " pcp=%u",
+ (tci & VLAN_PRIO_MASK) >> VLAN_PRIO_SHIFT);
+ if (fields & BIT(CFG80211_FLOW_F_VLAN_DEI))
+ seq_printf(s, " dei=%u", !!(tci & VLAN_CFI_MASK));
+ if (fields & BIT(CFG80211_FLOW_F_VLAN_VID))
+ seq_printf(s, " vid=%u", tci & VLAN_VID_MASK);
+
+ if (fields & BIT(CFG80211_FLOW_F_IP_VERSION))
+ seq_printf(s, " ipv%u", key->ip_version);
+
+ if (fields & BIT(CFG80211_FLOW_F_IP_SRC))
+ seq_printf(s, key->ip_version == 6 ? " src=%pI6" : " src=%pI4",
+ &key->src);
+ if (fields & BIT(CFG80211_FLOW_F_IP_DST))
+ seq_printf(s, key->ip_version == 6 ? " dst=%pI6" : " dst=%pI4",
+ &key->dst);
+
+ if (fields & BIT(CFG80211_FLOW_F_SRC_PORT))
+ seq_printf(s, " sport=%u", be16_to_cpu(key->src_port));
+ if (fields & BIT(CFG80211_FLOW_F_DST_PORT))
+ seq_printf(s, " dport=%u", be16_to_cpu(key->dst_port));
+
+ if (fields & BIT(CFG80211_FLOW_F_DSCP))
+ seq_printf(s, " dscp=%u", key->dscp);
+ if (fields & BIT(CFG80211_FLOW_F_PROTO))
+ seq_printf(s, " proto=%u", key->proto);
+ if (fields & BIT(CFG80211_FLOW_F_FLOW_LABEL))
+ seq_printf(s, " flowlabel=0x%05x",
+ be32_to_cpu(key->flow_label));
+}
+
+/* The set can be replaced during a walk, so look it up again at every step */
+static void *sta_scs_seq_start(struct seq_file *s, loff_t *pos)
+ __acquires(RCU)
+{
+ struct sta_info *sta = s->private;
+ struct ieee80211_scs_sta *scs;
+
+ rcu_read_lock();
+ scs = rcu_dereference(sta->scs);
+ if (!scs || *pos >= scs->n_rules)
+ return NULL;
+
+ return scs->rule[*pos];
+}
+
+static void *sta_scs_seq_next(struct seq_file *s, void *v, loff_t *pos)
+{
+ struct sta_info *sta = s->private;
+ struct ieee80211_scs_sta *scs = rcu_dereference(sta->scs);
+
+ ++*pos;
+ if (!scs || *pos >= scs->n_rules)
+ return NULL;
+
+ return scs->rule[*pos];
+}
+
+static void sta_scs_seq_stop(struct seq_file *s, void *v)
+ __releases(RCU)
+{
+ rcu_read_unlock();
+}
+
+static int sta_scs_seq_show(struct seq_file *s, void *v)
+{
+ static const char * const processing[] = {
+ [CFG80211_TCLAS_PROCESSING_ALL] = "all",
+ [CFG80211_TCLAS_PROCESSING_ANY] = "any",
+ [CFG80211_TCLAS_PROCESSING_DEFAULT] = "default",
+ };
+ const struct cfg80211_scs_desc *desc = v;
+ const char *proc = "none";
+ u8 i;
+
+ if (desc->tclas_processing < ARRAY_SIZE(processing))
+ proc = processing[desc->tclas_processing];
+
+ seq_printf(s, "scsid %u up %u processing %s", desc->id, desc->up, proc);
+
+ if (desc->qos_char)
+ seq_printf(s, " qos_char %u direction %u", desc->qos_char_len,
+ ieee80211_qos_char_direction(desc->qos_char));
+
+ seq_putc(s, '\n');
+
+ for (i = 0; i < desc->n_tclas; i++) {
+ const struct cfg80211_tclas *t = &desc->tclas[i];
+
+ seq_printf(s, " type %u", t->type);
+ sta_flow_key_show(s, &t->key, t->fields);
+ seq_putc(s, '\n');
+ }
+
+ return 0;
+}
+
+static const struct seq_operations sta_scs_seq_ops = {
+ .start = sta_scs_seq_start,
+ .next = sta_scs_seq_next,
+ .stop = sta_scs_seq_stop,
+ .show = sta_scs_seq_show,
+};
+
+static int sta_scs_open(struct inode *inode, struct file *file)
+{
+ struct seq_file *s;
+ int ret;
+
+ ret = seq_open(file, &sta_scs_seq_ops);
+ if (ret)
+ return ret;
+
+ s = file->private_data;
+ s->private = inode->i_private;
+
+ return 0;
+}
+
+static const struct file_operations sta_scs_ops = {
+ .open = sta_scs_open,
+ .read = seq_read,
+ .llseek = seq_lseek,
+ .release = seq_release,
+};
+
+struct sta_mscs_iter {
+ struct sta_info *sta;
+ struct ieee80211_mscs_sta *mscs;
+};
+
+/* The entry list is not an RCU list, so the whole walk holds the lock */
+static void *sta_mscs_seq_start(struct seq_file *s, loff_t *pos)
+ __acquires(RCU)
+{
+ struct sta_mscs_iter *iter = s->private;
+
+ rcu_read_lock();
+ iter->mscs = rcu_dereference(iter->sta->mscs);
+ if (!iter->mscs)
+ return NULL;
+
+ spin_lock_bh(&iter->mscs->lock);
+
+ if (*pos == 0)
+ return SEQ_START_TOKEN;
+
+ return seq_list_start(&iter->mscs->entries, *pos - 1);
+}
+
+static void *sta_mscs_seq_next(struct seq_file *s, void *v, loff_t *pos)
+{
+ struct sta_mscs_iter *iter = s->private;
+
+ if (v == SEQ_START_TOKEN) {
+ ++*pos;
+ return seq_list_start(&iter->mscs->entries, 0);
+ }
+
+ return seq_list_next(v, &iter->mscs->entries, pos);
+}
+
+static void sta_mscs_seq_stop(struct seq_file *s, void *v)
+ __releases(RCU)
+{
+ struct sta_mscs_iter *iter = s->private;
+
+ if (iter->mscs)
+ spin_unlock_bh(&iter->mscs->lock);
+
+ rcu_read_unlock();
+}
+
+static int sta_mscs_seq_show(struct seq_file *s, void *v)
+{
+ struct sta_mscs_iter *iter = s->private;
+ struct ieee80211_mscs_sta *mscs = iter->mscs;
+ struct ieee80211_flow_entry *entry;
+
+ if (v == SEQ_START_TOKEN) {
+ seq_printf(s,
+ "up_bitmap 0x%02x up_limit %u timeout %ums entries %u\n",
+ mscs->up_bitmap, mscs->up_limit,
+ jiffies_to_msecs(mscs->timeout), mscs->n_entries);
+ return 0;
+ }
+
+ entry = list_entry(v, struct ieee80211_flow_entry, list);
+
+ seq_printf(s, " up %u age %ums", READ_ONCE(entry->up),
+ jiffies_to_msecs(jiffies - READ_ONCE(entry->last_update)));
+ sta_flow_key_show(s, &entry->hkey.key, mscs->layout);
+ seq_putc(s, '\n');
+
+ return 0;
+}
+
+static const struct seq_operations sta_mscs_seq_ops = {
+ .start = sta_mscs_seq_start,
+ .next = sta_mscs_seq_next,
+ .stop = sta_mscs_seq_stop,
+ .show = sta_mscs_seq_show,
+};
+
+static int sta_mscs_open(struct inode *inode, struct file *file)
+{
+ struct sta_mscs_iter *iter;
+
+ iter = __seq_open_private(file, &sta_mscs_seq_ops, sizeof(*iter));
+ if (!iter)
+ return -ENOMEM;
+
+ iter->sta = inode->i_private;
+
+ return 0;
+}
+
+static const struct file_operations sta_mscs_ops = {
+ .open = sta_mscs_open,
+ .read = seq_read,
+ .llseek = seq_lseek,
+ .release = seq_release_private,
+};
+
#define AQM_TXQ_ENTRY_LEN 130
static ssize_t sta_aqm_read(struct file *file, char __user *userbuf,
@@ -1252,6 +1490,8 @@ void ieee80211_sta_debugfs_add(struct sta_info *sta)
sta->debugfs_dir = debugfs_create_dir(mac, stations_dir);
DEBUGFS_ADD(flags);
+ DEBUGFS_ADD(scs);
+ DEBUGFS_ADD(mscs);
DEBUGFS_ADD(aid);
DEBUGFS_ADD(num_ps_buf_frames);
DEBUGFS_ADD(last_seq_ctrl);
diff --git a/net/mac80211/driver-ops.h b/net/mac80211/driver-ops.h
index f1c0b87fddd5f..f7684e8db0251 100644
--- a/net/mac80211/driver-ops.h
+++ b/net/mac80211/driver-ops.h
@@ -1552,6 +1552,32 @@ static inline void drv_del_nan_func(struct ieee80211_local *local,
trace_drv_return_void(local);
}
+static inline int drv_sta_set_scs(struct ieee80211_local *local,
+ struct ieee80211_sub_if_data *sdata,
+ struct sta_info *sta,
+ struct cfg80211_scs_desc * const *desc,
+ struct cfg80211_scs_result *res, u8 n_desc)
+{
+ int ret;
+
+ might_sleep();
+ lockdep_assert_wiphy(local->hw.wiphy);
+
+ if (!local->ops->sta_set_scs || !sta->uploaded)
+ return 0;
+
+ sdata = get_bss_sdata(sdata);
+ if (!check_sdata_in_driver(sdata))
+ return -EIO;
+
+ trace_drv_sta_set_scs(local, sdata, &sta->sta, n_desc);
+ ret = local->ops->sta_set_scs(&local->hw, &sdata->vif, &sta->sta, desc,
+ res, n_desc);
+ trace_drv_return_int(local, ret);
+
+ return ret;
+}
+
static inline int drv_set_tid_config(struct ieee80211_local *local,
struct ieee80211_sub_if_data *sdata,
struct ieee80211_sta *sta,
diff --git a/net/mac80211/ieee80211_i.h b/net/mac80211/ieee80211_i.h
index 10767666c45c0..e0a7e9bb75a0d 100644
--- a/net/mac80211/ieee80211_i.h
+++ b/net/mac80211/ieee80211_i.h
@@ -345,6 +345,10 @@ struct ieee80211_if_ap {
bool multicast_to_unicast;
bool active;
+
+ /* learned MSCS entries of every station of this BSS, and its VLANs */
+ struct rhashtable flow_tbl;
+ struct wiphy_delayed_work flow_gc_work;
};
struct ieee80211_if_vlan {
@@ -2214,6 +2218,19 @@ void ieee80211_recalc_txpower(struct ieee80211_link_data *link,
bool update_bss);
void ieee80211_recalc_offload(struct ieee80211_local *local);
+/* SCS and MSCS */
+int ieee80211_set_scs(struct wiphy *wiphy, struct net_device *dev,
+ const u8 *peer, struct cfg80211_scs_desc * const *desc,
+ struct cfg80211_scs_result *res, u8 n_desc);
+int ieee80211_set_mscs(struct wiphy *wiphy, struct net_device *dev,
+ const u8 *peer, struct cfg80211_mscs_desc *desc);
+void ieee80211_sta_scs_reconfig(struct sta_info *sta);
+void ieee80211_sta_scs_free(struct sta_info *sta);
+bool ieee80211_flow_classify(struct sta_info *sta, struct sk_buff *skb);
+void ieee80211_flow_learn(struct ieee80211_rx_data *rx);
+int ieee80211_flow_tbl_init(struct ieee80211_sub_if_data *sdata);
+void ieee80211_flow_tbl_destroy(struct ieee80211_sub_if_data *sdata);
+
/* link handling */
void ieee80211_link_setup(struct ieee80211_link_data *link);
void ieee80211_link_init(struct ieee80211_sub_if_data *sdata,
diff --git a/net/mac80211/iface.c b/net/mac80211/iface.c
index 8535dd478633c..7e1f97e1bfcd3 100644
--- a/net/mac80211/iface.c
+++ b/net/mac80211/iface.c
@@ -542,6 +542,8 @@ static void ieee80211_do_stop(struct ieee80211_sub_if_data *sdata, bool going_do
atomic_dec(&local->iff_allmultis);
if (sdata->vif.type == NL80211_IFTYPE_AP) {
+ ieee80211_flow_tbl_destroy(sdata);
+
local->fif_pspoll--;
local->fif_probe_req--;
} else if (sdata->vif.type == NL80211_IFTYPE_ADHOC) {
@@ -1435,6 +1437,10 @@ int ieee80211_do_open(struct wireless_dev *wdev, bool coming_up)
break;
}
case NL80211_IFTYPE_AP:
+ res = ieee80211_flow_tbl_init(sdata);
+ if (res)
+ return res;
+ break;
case NL80211_IFTYPE_MESH_POINT:
case NL80211_IFTYPE_STATION:
case NL80211_IFTYPE_MONITOR:
@@ -1469,7 +1475,7 @@ int ieee80211_do_open(struct wireless_dev *wdev, bool coming_up)
* no need to worry about AP_VLAN/NAN_DATA cleanup since
* in that case we can't have open_count == 0
*/
- return res;
+ goto err_flow_tbl;
}
ieee80211_led_radio(local, true);
ieee80211_mod_tpt_led_trig(local,
@@ -1645,6 +1651,9 @@ int ieee80211_do_open(struct wireless_dev *wdev, bool coming_up)
list_del(&sdata->u.vlan.list);
/* Might not be initialized yet, but it is harmless */
sdata->bss = NULL;
+ err_flow_tbl:
+ if (sdata->vif.type == NL80211_IFTYPE_AP)
+ ieee80211_flow_tbl_destroy(sdata);
return res;
}
diff --git a/net/mac80211/rx.c b/net/mac80211/rx.c
index b3990b7a72990..fb7450715d2d8 100644
--- a/net/mac80211/rx.c
+++ b/net/mac80211/rx.c
@@ -2790,6 +2790,9 @@ ieee80211_deliver_skb(struct ieee80211_rx_data *rx)
u64_stats_update_end(&rx->link_sta->rx_stats.syncp);
}
+ if (rx->sta)
+ ieee80211_flow_learn(rx);
+
if ((sdata->vif.type == NL80211_IFTYPE_AP ||
sdata->vif.type == NL80211_IFTYPE_AP_VLAN) &&
!(sdata->flags & IEEE80211_SDATA_DONT_BRIDGE_PACKETS) &&
@@ -4995,6 +4998,8 @@ static void ieee80211_rx_8023(struct ieee80211_rx_data *rx,
u64_stats_add(&stats->bytes, orig_len);
u64_stats_update_end(&stats->syncp);
+ ieee80211_flow_learn(rx);
+
if (fast_rx->internal_forward) {
struct sk_buff *xmit_skb = NULL;
if (is_multicast_ether_addr(da)) {
diff --git a/net/mac80211/scs.c b/net/mac80211/scs.c
new file mode 100644
index 0000000000000..076e97cddbf1a
--- /dev/null
+++ b/net/mac80211/scs.c
@@ -0,0 +1,574 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Stream classification service (SCS) and mirrored SCS (MSCS)
+ *
+ * Copyright (C) 2026 Felix Fietkau <nbd@nbd.name>
+ */
+#include <linux/ieee80211.h>
+#include <net/cfg80211.h>
+#include "driver-ops.h"
+#include "ieee80211_i.h"
+#include "sta_info.h"
+
+/* An hour, in TUs of 1024 microseconds */
+#define IEEE80211_MSCS_TIMEOUT_MAX_TU 3515625
+
+/* Limit on learned MSCS entries per station */
+#define IEEE80211_MSCS_ENTRIES_MAX 64
+
+/* The hashed bytes must hold no padding between the peer and the key */
+static_assert(offsetofend(struct ieee80211_flow_hkey, key) ==
+ sizeof(struct sta_info *) + sizeof(struct cfg80211_flow_key));
+
+static const struct rhashtable_params flow_rht_params = {
+ .head_offset = offsetof(struct ieee80211_flow_entry, node),
+ .key_offset = offsetof(struct ieee80211_flow_entry, hkey),
+ .key_len = offsetofend(struct ieee80211_flow_hkey, key),
+ .automatic_shrinking = true,
+};
+
+static size_t ieee80211_scs_rule_size(const struct cfg80211_scs_desc *desc)
+{
+ return ALIGN(struct_size(desc, tclas, desc->n_tclas) +
+ desc->qos_char_len, __alignof__(*desc));
+}
+
+static void *ieee80211_scs_rule_copy(const struct cfg80211_scs_desc *src,
+ void *pos)
+{
+ size_t len = struct_size(src, tclas, src->n_tclas);
+ struct cfg80211_scs_desc *dst = pos;
+
+ memcpy(dst, src, len);
+ /* A stored rule is always handed back to the driver as an install */
+ dst->req_type = NL80211_SCS_REQ_ADD;
+ pos += len;
+
+ if (src->qos_char_len) {
+ memcpy(pos, src->qos_char, src->qos_char_len);
+ dst->qos_char = pos;
+ } else {
+ dst->qos_char = NULL;
+ }
+
+ return PTR_ALIGN(pos + src->qos_char_len, __alignof__(*dst));
+}
+
+static u8 ieee80211_scs_rule_find(struct cfg80211_scs_desc * const *rule,
+ u8 n_rules, u8 id)
+{
+ u8 i;
+
+ for (i = 0; i < n_rules; i++)
+ if (rule[i]->id == id)
+ break;
+
+ return i;
+}
+
+static size_t ieee80211_scs_sta_head(unsigned int n_rules)
+{
+ struct ieee80211_scs_sta *scs;
+
+ return ALIGN(struct_size(scs, rule, n_rules),
+ __alignof__(struct cfg80211_scs_desc));
+}
+
+/*
+ * Size for every rule that can end up in the set, so that no allocation
+ * fails after the driver programmed the accepted descriptors.
+ */
+static size_t ieee80211_scs_sta_size(const struct ieee80211_scs_sta *old,
+ struct cfg80211_scs_desc * const *desc,
+ u8 n_desc, unsigned int n_alloc)
+{
+ size_t size = ieee80211_scs_sta_head(n_alloc);
+ unsigned int i;
+
+ for (i = 0; old && i < old->n_rules; i++)
+ size += ieee80211_scs_rule_size(old->rule[i]);
+
+ for (i = 0; i < n_desc; i++)
+ size += ieee80211_scs_rule_size(desc[i]);
+
+ return size;
+}
+
+/*
+ * @rule points into the old set and into the request, so copy each rule.
+ * The block was sized for @n_alloc rules.
+ */
+static void ieee80211_scs_sta_fill(struct ieee80211_scs_sta *scs,
+ struct cfg80211_scs_desc * const *rule,
+ u8 n_rules, unsigned int n_alloc)
+{
+ void *pos = (void *)scs + ieee80211_scs_sta_head(n_alloc);
+ u8 i;
+
+ /* Set before the array is filled, for __counted_by() */
+ scs->n_rules = n_rules;
+
+ for (i = 0; i < n_rules; i++) {
+ scs->rule[i] = pos;
+ pos = ieee80211_scs_rule_copy(rule[i], pos);
+ }
+}
+
+/**
+ * ieee80211_flow_classify - give an MSDU the user priority of its stream
+ *
+ * @sta: the receiver
+ * @skb: the MSDU, in IEEE 802.3 format
+ *
+ * Return: %true when it set skb->priority, %false to leave the frame to the
+ * QoS map.
+ */
+bool ieee80211_flow_classify(struct sta_info *sta, struct sk_buff *skb)
+{
+ struct ieee80211_flow_entry *entry;
+ struct cfg80211_scs_verdict verdict;
+ struct ieee80211_mscs_sta *mscs;
+ struct ieee80211_flow_hkey hkey;
+ struct cfg80211_flow_info info;
+ struct ieee80211_scs_sta *scs;
+
+ scs = rcu_dereference(sta->scs);
+ mscs = rcu_dereference(sta->mscs);
+ if (!scs && !mscs)
+ return false;
+
+ if (!cfg80211_flow_parse(skb, &info))
+ return false;
+
+ if (scs) {
+ cfg80211_scs_evaluate(scs->rule, scs->n_rules, &info,
+ &verdict);
+ if (verdict.match) {
+ skb->priority = verdict.up;
+
+ return true;
+ }
+ }
+
+ /* An SCS match takes precedence over the MSCS */
+ if (!mscs)
+ return false;
+
+ hkey.sta = sta;
+ if (!cfg80211_flow_key_build(&info, mscs->layout, CFG80211_FLOW_AS_IS,
+ &hkey.key))
+ return false;
+
+ entry = rhashtable_lookup(&sta->sdata->bss->flow_tbl, &hkey,
+ flow_rht_params);
+ if (!entry)
+ return false;
+
+ skb->priority = min_t(u8, READ_ONCE(entry->up), mscs->up_limit);
+
+ return true;
+}
+
+static void ieee80211_flow_entry_free(struct ieee80211_if_ap *ap,
+ struct ieee80211_flow_entry *entry)
+{
+ rhashtable_remove_fast(&ap->flow_tbl, &entry->node, flow_rht_params);
+ list_del(&entry->list);
+ kfree_rcu(entry, rcu_head);
+}
+
+/* The caller holds @mscs->lock unless nothing else can reach the list */
+static void ieee80211_flow_purge(struct ieee80211_if_ap *ap,
+ struct ieee80211_mscs_sta *mscs)
+{
+ struct ieee80211_flow_entry *entry, *tmp;
+
+ list_for_each_entry_safe(entry, tmp, &mscs->entries, list)
+ ieee80211_flow_entry_free(ap, entry);
+
+ mscs->n_entries = 0;
+}
+
+/* Return the time until the next entry expires, zero when none is left */
+static unsigned long ieee80211_flow_gc_sta(struct ieee80211_if_ap *ap,
+ struct sta_info *sta)
+{
+ struct ieee80211_flow_entry *entry, *tmp;
+ struct ieee80211_mscs_sta *mscs;
+ unsigned long delay = 0;
+
+ mscs = wiphy_dereference(sta->local->hw.wiphy, sta->mscs);
+ if (!mscs)
+ return 0;
+
+ spin_lock_bh(&mscs->lock);
+
+ list_for_each_entry_safe(entry, tmp, &mscs->entries, list) {
+ unsigned long dead = READ_ONCE(entry->last_update) +
+ mscs->timeout;
+
+ if (time_after_eq(jiffies, dead)) {
+ ieee80211_flow_entry_free(ap, entry);
+ mscs->n_entries--;
+ continue;
+ }
+
+ if (!delay || time_before(dead, jiffies + delay))
+ delay = dead - jiffies;
+ }
+
+ spin_unlock_bh(&mscs->lock);
+
+ return delay;
+}
+
+static void ieee80211_flow_gc_work(struct wiphy *wiphy, struct wiphy_work *work)
+{
+ struct ieee80211_sub_if_data *sdata;
+ struct ieee80211_if_ap *ap;
+ unsigned long delay = 0;
+ struct sta_info *sta;
+
+ ap = container_of(work, struct ieee80211_if_ap, flow_gc_work.work);
+ sdata = container_of(ap, struct ieee80211_sub_if_data, u.ap);
+
+ list_for_each_entry(sta, &sdata->local->sta_list, list) {
+ unsigned long sta_delay;
+
+ if (sta->sdata->bss != ap)
+ continue;
+
+ sta_delay = ieee80211_flow_gc_sta(ap, sta);
+ if (sta_delay && (!delay || sta_delay < delay))
+ delay = sta_delay;
+ }
+
+ if (delay)
+ wiphy_delayed_work_queue(wiphy, &ap->flow_gc_work, delay);
+}
+
+int ieee80211_flow_tbl_init(struct ieee80211_sub_if_data *sdata)
+{
+ wiphy_delayed_work_init(&sdata->u.ap.flow_gc_work,
+ ieee80211_flow_gc_work);
+
+ return rhashtable_init(&sdata->u.ap.flow_tbl, &flow_rht_params);
+}
+
+static void ieee80211_flow_tbl_free(void *ptr, void *arg)
+{
+ struct ieee80211_flow_entry *entry = ptr;
+
+ kfree(entry);
+}
+
+void ieee80211_flow_tbl_destroy(struct ieee80211_sub_if_data *sdata)
+{
+ wiphy_delayed_work_cancel(sdata->local->hw.wiphy,
+ &sdata->u.ap.flow_gc_work);
+ rhashtable_free_and_destroy(&sdata->u.ap.flow_tbl,
+ ieee80211_flow_tbl_free, NULL);
+}
+
+static void ieee80211_flow_insert(struct ieee80211_sub_if_data *sdata,
+ struct ieee80211_mscs_sta *mscs,
+ const struct ieee80211_flow_hkey *hkey, u8 up)
+{
+ struct ieee80211_flow_entry *entry, *other;
+ bool first;
+
+ entry = kzalloc_obj(*entry, GFP_ATOMIC);
+ if (!entry)
+ return;
+
+ entry->hkey.sta = hkey->sta;
+ entry->hkey.key = hkey->key;
+ entry->up = up;
+ entry->last_update = jiffies;
+
+ spin_lock_bh(&mscs->lock);
+
+ /* Another MSCS may have been installed, which emptied this list */
+ if (rcu_dereference(hkey->sta->mscs) != mscs)
+ goto drop;
+
+ if (mscs->n_entries >= IEEE80211_MSCS_ENTRIES_MAX)
+ goto drop;
+
+ other = rhashtable_lookup_get_insert_fast(&sdata->bss->flow_tbl,
+ &entry->node,
+ flow_rht_params);
+ if (other) {
+ /* Concurrent insert of the same key, the last writer wins */
+ if (!IS_ERR(other)) {
+ WRITE_ONCE(other->up, up);
+ WRITE_ONCE(other->last_update, jiffies);
+ }
+
+ goto drop;
+ }
+
+ list_add_tail(&entry->list, &mscs->entries);
+ first = mscs->n_entries++ == 0;
+ spin_unlock_bh(&mscs->lock);
+
+ /* The work requeues itself while entries remain */
+ if (first)
+ wiphy_delayed_work_queue(sdata->local->hw.wiphy,
+ &sdata->bss->flow_gc_work, 1);
+
+ return;
+
+drop:
+ spin_unlock_bh(&mscs->lock);
+ kfree(entry);
+}
+
+/**
+ * ieee80211_flow_learn - record the user priority of an uplink flow
+ *
+ * @rx: the received frame, in IEEE 802.3 format
+ *
+ * The key is built for the downlink direction, so that the transmit path
+ * can look it up as is.
+ */
+void ieee80211_flow_learn(struct ieee80211_rx_data *rx)
+{
+ struct ieee80211_sub_if_data *sdata = rx->sdata;
+ const struct ethhdr *eth = (void *)rx->skb->data;
+ struct ieee80211_flow_hkey hkey;
+ struct ieee80211_mscs_sta *mscs;
+ struct ieee80211_flow_entry *entry;
+ struct cfg80211_flow_info info;
+ /* The TID from ieee80211_parse_qos(), or from a decap offload driver */
+ u32 up = rx->skb->priority;
+
+ if (sdata->vif.type != NL80211_IFTYPE_AP &&
+ sdata->vif.type != NL80211_IFTYPE_AP_VLAN)
+ return;
+
+ mscs = rcu_dereference(rx->sta->mscs);
+ if (!mscs)
+ return;
+
+ if (up > 7 || !(mscs->up_bitmap & BIT(up)))
+ return;
+
+ if (is_multicast_ether_addr(eth->h_dest) ||
+ !ether_addr_equal(eth->h_source, rx->sta->addr))
+ return;
+
+ if (!cfg80211_flow_parse(rx->skb, &info))
+ return;
+
+ hkey.sta = rx->sta;
+ if (!cfg80211_flow_key_build(&info, mscs->layout,
+ CFG80211_FLOW_MIRRORED, &hkey.key))
+ return;
+
+ entry = rhashtable_lookup(&sdata->bss->flow_tbl, &hkey,
+ flow_rht_params);
+ if (entry) {
+ WRITE_ONCE(entry->up, up);
+ WRITE_ONCE(entry->last_update, jiffies);
+ return;
+ }
+
+ ieee80211_flow_insert(sdata, mscs, &hkey, up);
+}
+
+int ieee80211_set_scs(struct wiphy *wiphy, struct net_device *dev,
+ const u8 *peer, struct cfg80211_scs_desc * const *desc,
+ struct cfg80211_scs_result *res, u8 n_desc)
+{
+ struct ieee80211_sub_if_data *sdata = IEEE80211_DEV_TO_SUB_IF(dev);
+ struct ieee80211_scs_sta *scs, *old;
+ struct cfg80211_scs_desc **rule;
+ struct sta_info *sta;
+ unsigned int n_alloc;
+ u8 i, n_rules = 0;
+ int ret;
+
+ lockdep_assert_wiphy(wiphy);
+
+ sta = sta_info_get_bss(sdata, peer);
+ if (!sta)
+ return -ENOENT;
+
+ old = wiphy_dereference(wiphy, sta->scs);
+
+ /* One rule per SCSID bounds the size of the new set */
+ n_alloc = (old ? old->n_rules : 0) + n_desc;
+
+ rule = kcalloc(n_alloc, sizeof(*rule), GFP_KERNEL);
+ if (!rule)
+ return -ENOMEM;
+
+ /* 255 rules with 255 classifiers each exceed what kmalloc hands out */
+ scs = kvzalloc(ieee80211_scs_sta_size(old, desc, n_desc, n_alloc),
+ GFP_KERNEL);
+ if (!scs) {
+ ret = -ENOMEM;
+ goto free;
+ }
+
+ if (sdata->local->ops->sta_set_scs && sta->uploaded) {
+ ret = drv_sta_set_scs(sdata->local, sdata, sta, desc, res,
+ n_desc);
+ if (ret)
+ goto free;
+ } else {
+ /* Without a driver, no traffic description can be served */
+ for (i = 0; i < n_desc; i++)
+ if (desc[i]->qos_char)
+ res[i].status = WLAN_STATUS_REQUEST_DECLINED;
+ }
+
+ if (old) {
+ n_rules = old->n_rules;
+ memcpy(rule, old->rule, n_rules * sizeof(*rule));
+ }
+
+ for (i = 0; i < n_desc; i++) {
+ u8 at = ieee80211_scs_rule_find(rule, n_rules, desc[i]->id);
+
+ if (desc[i]->req_type == NL80211_SCS_REQ_REMOVE) {
+ /* A removal is answered even when no rule matched */
+ res[i].status = WLAN_STATUS_TCLAS_PROCESSING_TERMINATED;
+
+ if (at < n_rules) {
+ n_rules--;
+ memmove(&rule[at], &rule[at + 1],
+ (n_rules - at) * sizeof(*rule));
+ }
+
+ continue;
+ }
+
+ if (res[i].status != WLAN_STATUS_SUCCESS)
+ continue;
+
+ if (at < n_rules)
+ rule[at] = desc[i];
+ else
+ rule[n_rules++] = desc[i];
+ }
+
+ if (n_rules) {
+ ieee80211_scs_sta_fill(scs, rule, n_rules, n_alloc);
+ } else {
+ kvfree(scs);
+ scs = NULL;
+ }
+
+ kfree(rule);
+
+ rcu_assign_pointer(sta->scs, scs);
+ if (old)
+ kvfree_rcu(old, rcu_head);
+
+ return 0;
+
+free:
+ kvfree(scs);
+ kfree(rule);
+
+ return ret;
+}
+
+int ieee80211_set_mscs(struct wiphy *wiphy, struct net_device *dev,
+ const u8 *peer, struct cfg80211_mscs_desc *desc)
+{
+ struct ieee80211_sub_if_data *sdata = IEEE80211_DEV_TO_SUB_IF(dev);
+ struct ieee80211_mscs_sta *mscs = NULL, *old;
+ struct sta_info *sta;
+ u32 tu;
+
+ lockdep_assert_wiphy(wiphy);
+
+ /* Only an AP provides MSCS */
+ if (!sdata->bss)
+ return -EOPNOTSUPP;
+
+ sta = sta_info_get_bss(sdata, peer);
+ if (!sta)
+ return -ENOENT;
+
+ if (desc->req_type != NL80211_SCS_REQ_REMOVE) {
+ mscs = kzalloc_obj(*mscs);
+ if (!mscs)
+ return -ENOMEM;
+
+ mscs->layout = desc->fields;
+ mscs->up_bitmap = desc->up_bitmap;
+ mscs->up_limit = desc->up_limit;
+
+ /*
+ * Cap at an hour to keep tu * 1024 within the unsigned int
+ * that usecs_to_jiffies() takes. A timeout of zero jiffies
+ * would expire an entry at the next collection.
+ */
+ tu = min(desc->stream_timeout, IEEE80211_MSCS_TIMEOUT_MAX_TU);
+ mscs->timeout = max(1UL, usecs_to_jiffies(tu * 1024));
+
+ spin_lock_init(&mscs->lock);
+ INIT_LIST_HEAD(&mscs->entries);
+ }
+
+ old = wiphy_dereference(wiphy, sta->mscs);
+ rcu_assign_pointer(sta->mscs, mscs);
+
+ if (old) {
+ /* The entries were keyed with the old layout */
+ spin_lock_bh(&old->lock);
+ ieee80211_flow_purge(sta->sdata->bss, old);
+ spin_unlock_bh(&old->lock);
+
+ kfree_rcu(old, rcu_head);
+ }
+
+ return 0;
+}
+
+/*
+ * A restarted device lost every stream, so program them again. A refusal
+ * changes nothing here: the rule stays and mac80211 keeps classifying for it.
+ */
+void ieee80211_sta_scs_reconfig(struct sta_info *sta)
+{
+ struct ieee80211_local *local = sta->local;
+ struct ieee80211_scs_sta *scs;
+ u8 i;
+
+ lockdep_assert_wiphy(local->hw.wiphy);
+
+ scs = wiphy_dereference(local->hw.wiphy, sta->scs);
+ if (!scs)
+ return;
+
+ /* One rule per call, so no result array has to be allocated */
+ for (i = 0; i < scs->n_rules; i++) {
+ struct cfg80211_scs_result res = {};
+
+ drv_sta_set_scs(local, sta->sdata, sta, &scs->rule[i], &res, 1);
+ }
+}
+
+void ieee80211_sta_scs_free(struct sta_info *sta)
+{
+ struct ieee80211_mscs_sta *mscs;
+ struct ieee80211_scs_sta *scs;
+
+ /* Nothing can reach the station any more, so no lock is needed */
+ scs = rcu_dereference_raw(sta->scs);
+ RCU_INIT_POINTER(sta->scs, NULL);
+ kvfree(scs);
+
+ mscs = rcu_dereference_raw(sta->mscs);
+ RCU_INIT_POINTER(sta->mscs, NULL);
+ if (!mscs)
+ return;
+
+ ieee80211_flow_purge(sta->sdata->bss, mscs);
+ kfree(mscs);
+}
diff --git a/net/mac80211/sta_info.c b/net/mac80211/sta_info.c
index fdf00cbf49d8b..e14d78b002f1c 100644
--- a/net/mac80211/sta_info.c
+++ b/net/mac80211/sta_info.c
@@ -508,6 +508,7 @@ void sta_info_free(struct ieee80211_local *local, struct sta_info *sta)
kfree(sta->mesh);
#endif
+ ieee80211_sta_scs_free(sta);
sta_info_free_link(&sta->deflink);
kfree(sta);
}
diff --git a/net/mac80211/sta_info.h b/net/mac80211/sta_info.h
index ee0d32877c5b4..6e65313fe4d9c 100644
--- a/net/mac80211/sta_info.h
+++ b/net/mac80211/sta_info.h
@@ -634,6 +634,79 @@ struct ieee80211_sta_removed_link_stats {
} pertid_stats;
};
+/**
+ * struct ieee80211_scs_sta - the SCS rule set of one station
+ *
+ * @rcu_head: for freeing
+ * @n_rules: number of entries in @rule
+ * @rule: the active descriptors, in the order they were installed
+ *
+ * One allocation holds this structure, the descriptors it points to and their
+ * QoS Characteristics octets, so the whole set has one lifetime.
+ */
+struct ieee80211_scs_sta {
+ struct rcu_head rcu_head;
+ u8 n_rules;
+ struct cfg80211_scs_desc *rule[] __counted_by(n_rules);
+};
+
+/**
+ * struct ieee80211_mscs_sta - the MSCS of one station
+ *
+ * @rcu_head: for freeing
+ * @layout: classifier parameters, a bitmap of &enum cfg80211_flow_field
+ * @up_bitmap: user priorities that the AP learns from, one bit each
+ * @up_limit: ceiling for the assigned user priority
+ * @timeout: minimum lifetime of a learned value, in jiffies
+ * @lock: protects @entries and @n_entries
+ * @entries: the learned entries of this station, for teardown
+ * @n_entries: number of entries in @entries
+ */
+struct ieee80211_mscs_sta {
+ struct rcu_head rcu_head;
+ u32 layout;
+ u8 up_bitmap;
+ u8 up_limit;
+ unsigned long timeout;
+
+ spinlock_t lock; /* protects @entries and @n_entries */
+ struct list_head entries;
+ u32 n_entries;
+};
+
+/**
+ * struct ieee80211_flow_hkey - what a learned entry is looked up by
+ *
+ * @sta: the peer, so that a flow of one station never answers for another
+ * @key: the masked tuple, in the downlink direction
+ *
+ * Not packed: the whole of it is compared, but a packed aggregate would force
+ * byte-wise access to @sta.
+ */
+struct ieee80211_flow_hkey {
+ struct sta_info *sta;
+ struct cfg80211_flow_key key;
+};
+
+/**
+ * struct ieee80211_flow_entry - the user priority learned for one flow
+ *
+ * @hkey: what it is looked up by
+ * @node: hash table linkage
+ * @list: linkage in &ieee80211_mscs_sta.entries
+ * @last_update: jiffies of the last uplink frame of this flow
+ * @up: the learned user priority
+ * @rcu_head: for freeing
+ */
+struct ieee80211_flow_entry {
+ struct ieee80211_flow_hkey hkey;
+ struct rhash_head node;
+ struct list_head list;
+ unsigned long last_update;
+ u8 up;
+ struct rcu_head rcu_head;
+};
+
/**
* struct sta_info - STA information
*
@@ -694,6 +767,8 @@ struct ieee80211_sta_removed_link_stats {
*
* @fast_tx: TX fastpath information
* @fast_rx: RX fastpath information
+ * @scs: SCS rule set, or %NULL
+ * @mscs: MSCS parameters, or %NULL
* @tdls_chandef: a TDLS peer can have a wider chandef that is compatible to
* the BSS one.
* @frags: fragment cache
@@ -730,6 +805,9 @@ struct sta_info {
struct ieee80211_fast_tx __rcu *fast_tx;
struct ieee80211_fast_rx __rcu *fast_rx;
+ struct ieee80211_scs_sta __rcu *scs;
+ struct ieee80211_mscs_sta __rcu *mscs;
+
#ifdef CONFIG_MAC80211_MESH
struct mesh_sta *mesh;
#endif
diff --git a/net/mac80211/trace.h b/net/mac80211/trace.h
index 562a4964afa3a..1e6026306fb2e 100644
--- a/net/mac80211/trace.h
+++ b/net/mac80211/trace.h
@@ -1023,6 +1023,33 @@ DECLARE_EVENT_CLASS(sta_event,
)
);
+TRACE_EVENT(drv_sta_set_scs,
+ TP_PROTO(struct ieee80211_local *local,
+ struct ieee80211_sub_if_data *sdata,
+ struct ieee80211_sta *sta, u8 n_desc),
+
+ TP_ARGS(local, sdata, sta, n_desc),
+
+ TP_STRUCT__entry(
+ LOCAL_ENTRY
+ VIF_ENTRY
+ STA_ENTRY
+ __field(u8, n_desc)
+ ),
+
+ TP_fast_assign(
+ LOCAL_ASSIGN;
+ VIF_ASSIGN;
+ STA_ASSIGN;
+ __entry->n_desc = n_desc;
+ ),
+
+ TP_printk(
+ LOCAL_PR_FMT VIF_PR_FMT STA_PR_FMT " %d descriptors",
+ LOCAL_PR_ARG, VIF_PR_ARG, STA_PR_ARG, __entry->n_desc
+ )
+);
+
DEFINE_EVENT(sta_event, drv_sta_statistics,
TP_PROTO(struct ieee80211_local *local,
struct ieee80211_sub_if_data *sdata,
diff --git a/net/mac80211/util.c b/net/mac80211/util.c
index e2b5b2b21c347..c97a3e00e2518 100644
--- a/net/mac80211/util.c
+++ b/net/mac80211/util.c
@@ -1744,6 +1744,8 @@ static void ieee80211_reconfig_stations(struct ieee80211_sub_if_data *sdata)
state < sta->sta_state; state++)
WARN_ON(drv_sta_state(local, sta->sdata, sta, state,
state + 1));
+
+ ieee80211_sta_scs_reconfig(sta);
}
}
diff --git a/net/mac80211/wme.c b/net/mac80211/wme.c
index 1601be5764145..9fcee1bc6ad59 100644
--- a/net/mac80211/wme.c
+++ b/net/mac80211/wme.c
@@ -174,6 +174,13 @@ u16 ieee80211_select_queue(struct ieee80211_sub_if_data *sdata,
goto downgrade;
}
+ /* A stream priority overrides the received one of a forwarded frame */
+ if (sta &&
+ (sdata->vif.type == NL80211_IFTYPE_AP ||
+ sdata->vif.type == NL80211_IFTYPE_AP_VLAN) &&
+ ieee80211_flow_classify(sta, skb))
+ goto downgrade;
+
/* use the data classifier to determine what 802.1d tag the
* data frame has */
qos_map = rcu_dereference(sdata->qos_map);
diff --git a/net/wireless/Makefile b/net/wireless/Makefile
index a77fd5ba63686..552f4557ca23d 100644
--- a/net/wireless/Makefile
+++ b/net/wireless/Makefile
@@ -8,7 +8,7 @@ obj-$(CONFIG_WEXT_PRIV) += wext-priv.o
cfg80211-y += core.o sysfs.o radiotap.o util.o reg.o scan.o nl80211.o
cfg80211-y += mlme.o ibss.o sme.o chan.o ethtool.o mesh.o ap.o trace.o ocb.o
-cfg80211-y += michael-mic.o pmsr.o
+cfg80211-y += michael-mic.o pmsr.o scs.o
cfg80211-$(CONFIG_OF) += of.o
cfg80211-$(CONFIG_CFG80211_DEBUGFS) += debugfs.o
cfg80211-$(CONFIG_CFG80211_WEXT) += wext-compat.o wext-sme.o
diff --git a/net/wireless/core.c b/net/wireless/core.c
index cde3ca85494de..d7b47cbbf5427 100644
--- a/net/wireless/core.c
+++ b/net/wireless/core.c
@@ -999,6 +999,16 @@ int wiphy_register(struct wiphy *wiphy)
rdev->ops->update_connect_params))
return -EINVAL;
+ if (WARN_ON(wiphy_ext_feature_isset(&rdev->wiphy,
+ NL80211_EXT_FEATURE_SCS) &&
+ !rdev->ops->set_scs))
+ return -EINVAL;
+
+ if (WARN_ON(wiphy_ext_feature_isset(&rdev->wiphy,
+ NL80211_EXT_FEATURE_MSCS) &&
+ !rdev->ops->set_mscs))
+ return -EINVAL;
+
if (wiphy->addresses)
memcpy(wiphy->perm_addr, wiphy->addresses[0].addr, ETH_ALEN);
diff --git a/net/wireless/core.h b/net/wireless/core.h
index 6138d207caf41..9e8c2c42d67a7 100644
--- a/net/wireless/core.h
+++ b/net/wireless/core.h
@@ -648,6 +648,14 @@ struct cfg80211_colocated_ap {
s8 psd_20;
};
+int cfg80211_tclas_count(const u8 *elems, size_t len);
+int cfg80211_parse_tclas(const u8 *elems, size_t len,
+ struct cfg80211_tclas *out, u8 n_tclas,
+ enum cfg80211_tclas_processing *processing);
+int cfg80211_parse_tclas_mask(const u8 *elems, size_t len, u32 *fields);
+bool cfg80211_scs_desc_valid(const struct cfg80211_scs_desc *desc);
+bool cfg80211_mscs_desc_valid(const struct cfg80211_mscs_desc *desc);
+
#if IS_ENABLED(CONFIG_CFG80211_KUNIT_TEST)
#define EXPORT_SYMBOL_IF_CFG80211_KUNIT(sym) EXPORT_SYMBOL_IF_KUNIT(sym)
#define VISIBLE_IF_CFG80211_KUNIT
diff --git a/net/wireless/nl80211.c b/net/wireless/nl80211.c
index 85c010793c53e..b7a16a478ad04 100644
--- a/net/wireless/nl80211.c
+++ b/net/wireless/nl80211.c
@@ -624,6 +624,31 @@ sar_policy[NL80211_SAR_ATTR_MAX + 1] = {
[NL80211_SAR_ATTR_SPECS] = NLA_POLICY_NESTED_ARRAY(sar_specs_policy),
};
+/*
+ * The element parsers validate the binary attributes, so that a malformed
+ * element is answered with a status code instead of a netlink error.
+ */
+static const struct nla_policy
+nl80211_scs_desc_policy[NL80211_SCS_DESC_ATTR_MAX + 1] = {
+ [NL80211_SCS_DESC_ATTR_ID] = NLA_POLICY_MIN(NLA_U8, 1),
+ [NL80211_SCS_DESC_ATTR_REQ_TYPE] =
+ NLA_POLICY_MAX(NLA_U8, NL80211_SCS_REQ_CHANGE),
+ [NL80211_SCS_DESC_ATTR_UP] = NLA_POLICY_MAX(NLA_U8, 7),
+ [NL80211_SCS_DESC_ATTR_TCLAS] = { .type = NLA_BINARY },
+ [NL80211_SCS_DESC_ATTR_QOS_CHAR] = { .type = NLA_BINARY },
+ [NL80211_SCS_DESC_ATTR_STATUS] = { .type = NLA_REJECT },
+};
+
+static const struct nla_policy
+nl80211_mscs_desc_policy[NL80211_MSCS_DESC_ATTR_MAX + 1] = {
+ [NL80211_MSCS_DESC_ATTR_REQ_TYPE] =
+ NLA_POLICY_MAX(NLA_U8, NL80211_SCS_REQ_CHANGE),
+ [NL80211_MSCS_DESC_ATTR_UP_BITMAP] = { .type = NLA_U8 },
+ [NL80211_MSCS_DESC_ATTR_UP_LIMIT] = NLA_POLICY_MAX(NLA_U8, 7),
+ [NL80211_MSCS_DESC_ATTR_STREAM_TIMEOUT] = { .type = NLA_U32 },
+ [NL80211_MSCS_DESC_ATTR_TCLAS_MASK] = { .type = NLA_BINARY },
+};
+
static const struct nla_policy
nl80211_mbssid_config_policy[NL80211_MBSSID_CONFIG_ATTR_MAX + 1] = {
[NL80211_MBSSID_CONFIG_ATTR_MAX_INTERFACES] = NLA_POLICY_MIN(NLA_U8, 2),
@@ -1098,6 +1123,10 @@ static const struct nla_policy nl80211_policy[NUM_NL80211_ATTR] = {
NLA_POLICY_FULL_RANGE(NLA_U32, &nl80211_punct_bitmap_range),
[NL80211_ATTR_STA_DUMP_LINK_STATS] = { .type = NLA_FLAG },
[NL80211_ATTR_FRAME_NO_STA] = { .type = NLA_FLAG },
+ [NL80211_ATTR_SCS_DESCRIPTORS] =
+ NLA_POLICY_NESTED_ARRAY(nl80211_scs_desc_policy),
+ [NL80211_ATTR_MSCS_DESCRIPTOR] =
+ NLA_POLICY_NESTED(nl80211_mscs_desc_policy),
};
/* policy for the key attributes */
@@ -18288,6 +18317,390 @@ static int nl80211_set_qos_map(struct sk_buff *skb,
return ret;
}
+#define NL80211_MAX_SCS_DESC 255
+
+/* A station can install the traffic description of its uplink stream */
+static int nl80211_scs_iftype_ok(struct net_device *dev)
+{
+ switch (dev->ieee80211_ptr->iftype) {
+ case NL80211_IFTYPE_AP:
+ case NL80211_IFTYPE_P2P_GO:
+ case NL80211_IFTYPE_STATION:
+ return 0;
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+/* Only an AP mirrors an uplink priority onto the downlink */
+static int nl80211_mscs_iftype_ok(struct net_device *dev)
+{
+ switch (dev->ieee80211_ptr->iftype) {
+ case NL80211_IFTYPE_AP:
+ case NL80211_IFTYPE_P2P_GO:
+ return 0;
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static u16 nl80211_scs_status(int err)
+{
+ switch (err) {
+ case -EOPNOTSUPP:
+ return WLAN_STATUS_REQUESTED_TCLAS_NOT_SUPPORTED_BY_AP;
+ case -ENOSPC:
+ return WLAN_STATUS_INSUFFICIENT_TCLAS_PROCESSING_RESOURCES;
+ default:
+ return WLAN_STATUS_REQUEST_DECLINED;
+ }
+}
+
+static void nl80211_free_scs_desc(struct cfg80211_scs_desc **desc,
+ unsigned int n)
+{
+ unsigned int i;
+
+ for (i = 0; i < n; i++)
+ kfree(desc[i]);
+
+ kfree(desc);
+}
+
+static struct cfg80211_scs_desc *
+nl80211_parse_scs_desc(struct nlattr *attr, struct cfg80211_scs_result *res,
+ struct genl_info *info)
+{
+ enum cfg80211_tclas_processing processing =
+ CFG80211_TCLAS_PROCESSING_ABSENT;
+ struct nlattr *tb[NL80211_SCS_DESC_ATTR_MAX + 1];
+ const struct ieee80211_qos_char_elem *qos_char = NULL;
+ struct nlattr *qos_attr, *tclas_attr;
+ struct cfg80211_scs_desc *desc;
+ bool wants_up = true;
+ int n_tclas = 0;
+ bool remove;
+ int err;
+
+ err = nla_parse_nested(tb, NL80211_SCS_DESC_ATTR_MAX, attr,
+ nl80211_scs_desc_policy, info->extack);
+ if (err)
+ return ERR_PTR(err);
+
+ if (!tb[NL80211_SCS_DESC_ATTR_ID] ||
+ !tb[NL80211_SCS_DESC_ATTR_REQ_TYPE]) {
+ GENL_SET_ERR_MSG(info,
+ "SCS descriptor without an ID or a request type");
+ return ERR_PTR(-EINVAL);
+ }
+
+ remove = nla_get_u8(tb[NL80211_SCS_DESC_ATTR_REQ_TYPE]) ==
+ NL80211_SCS_REQ_REMOVE;
+
+ /* A removal names only an SCSID, the elements are ignored */
+ qos_attr = remove ? NULL : tb[NL80211_SCS_DESC_ATTR_QOS_CHAR];
+ if (qos_attr && ieee80211_qos_char_size_ok(nla_data(qos_attr),
+ nla_len(qos_attr)))
+ qos_char = nla_data(qos_attr);
+
+ /* An uplink or direct link descriptor carries no user priority */
+ if (qos_char && ieee80211_qos_char_direction(qos_char) !=
+ IEEE80211_QOS_CHAR_DIR_DOWNLINK)
+ wants_up = false;
+
+ /* Decline only this descriptor, the rest of the request stands */
+ if (!remove && (!qos_attr || qos_char) &&
+ wants_up != !!tb[NL80211_SCS_DESC_ATTR_UP])
+ err = -EINVAL;
+
+ tclas_attr = remove ? NULL : tb[NL80211_SCS_DESC_ATTR_TCLAS];
+ if (!err && tclas_attr) {
+ n_tclas = cfg80211_tclas_count(nla_data(tclas_attr),
+ nla_len(tclas_attr));
+ if (n_tclas < 0) {
+ err = n_tclas;
+ n_tclas = 0;
+ }
+ }
+
+ desc = kzalloc(struct_size(desc, tclas, n_tclas), GFP_KERNEL);
+ if (!desc)
+ return ERR_PTR(-ENOMEM);
+
+ desc->id = nla_get_u8(tb[NL80211_SCS_DESC_ATTR_ID]);
+ desc->req_type = nla_get_u8(tb[NL80211_SCS_DESC_ATTR_REQ_TYPE]);
+ desc->up = nla_get_u8_default(tb[NL80211_SCS_DESC_ATTR_UP], 0);
+ desc->n_tclas = n_tclas;
+
+ if (!err && tclas_attr) {
+ err = cfg80211_parse_tclas(nla_data(tclas_attr),
+ nla_len(tclas_attr), desc->tclas,
+ n_tclas, &processing);
+ if (err) {
+ desc->n_tclas = 0;
+ processing = CFG80211_TCLAS_PROCESSING_ABSENT;
+ }
+ }
+
+ desc->tclas_processing = processing;
+
+ if (!err && qos_attr) {
+ if (!qos_char) {
+ NL_SET_ERR_MSG(info->extack,
+ "bad QoS Characteristics element");
+ err = -EINVAL;
+ } else {
+ desc->qos_char = qos_char;
+ desc->qos_char_len = nla_len(qos_attr);
+ }
+ }
+
+ if (err)
+ res->status = nl80211_scs_status(err);
+ else if (!cfg80211_scs_desc_valid(desc))
+ res->status = WLAN_STATUS_REQUEST_DECLINED;
+
+ return desc;
+}
+
+static int nl80211_set_scs_reply(struct genl_info *info,
+ struct cfg80211_scs_desc * const *desc,
+ const struct cfg80211_scs_result *res,
+ unsigned int n_desc)
+{
+ struct nlattr *list, *entry;
+ struct sk_buff *msg;
+ unsigned int i;
+ size_t size;
+ void *hdr;
+
+ size = nla_total_size(nla_total_size(sizeof(u8)) +
+ nla_total_size(sizeof(u16)));
+
+ msg = nlmsg_new(NLMSG_DEFAULT_SIZE + n_desc * size, GFP_KERNEL);
+ if (!msg)
+ return -ENOMEM;
+
+ hdr = nl80211hdr_put(msg, info->snd_portid, info->snd_seq, 0,
+ NL80211_CMD_SET_SCS);
+ if (!hdr)
+ goto nla_put_failure;
+
+ list = nla_nest_start(msg, NL80211_ATTR_SCS_DESCRIPTORS);
+ if (!list)
+ goto nla_put_failure;
+
+ for (i = 0; i < n_desc; i++) {
+ entry = nla_nest_start(msg, i + 1);
+ if (!entry)
+ goto nla_put_failure;
+
+ if (nla_put_u8(msg, NL80211_SCS_DESC_ATTR_ID, desc[i]->id) ||
+ nla_put_u16(msg, NL80211_SCS_DESC_ATTR_STATUS,
+ res[i].status))
+ goto nla_put_failure;
+
+ nla_nest_end(msg, entry);
+ }
+
+ nla_nest_end(msg, list);
+ genlmsg_end(msg, hdr);
+
+ return genlmsg_reply(msg, info);
+
+nla_put_failure:
+ nlmsg_free(msg);
+
+ return -ENOBUFS;
+}
+
+static int nl80211_set_scs(struct sk_buff *skb, struct genl_info *info)
+{
+ struct cfg80211_registered_device *rdev = info->user_ptr[0];
+ struct net_device *dev = info->user_ptr[1];
+ unsigned int i, n_desc = 0, n_apply = 0;
+ struct cfg80211_scs_result *res;
+ struct cfg80211_scs_desc **desc;
+ struct nlattr *nla;
+ int rem, err;
+
+ if (!wiphy_ext_feature_isset(&rdev->wiphy, NL80211_EXT_FEATURE_SCS))
+ return -EOPNOTSUPP;
+
+ if (!info->attrs[NL80211_ATTR_MAC] ||
+ !info->attrs[NL80211_ATTR_SCS_DESCRIPTORS])
+ return -EINVAL;
+
+ if (info->attrs[NL80211_ATTR_MLO_LINK_ID]) {
+ GENL_SET_ERR_MSG(info,
+ "SCS rules belong to the MLD, not to a link");
+ return -EINVAL;
+ }
+
+ err = nl80211_scs_iftype_ok(dev);
+ if (err)
+ return err;
+
+ nla_for_each_nested(nla, info->attrs[NL80211_ATTR_SCS_DESCRIPTORS], rem)
+ n_desc++;
+
+ if (!n_desc || n_desc > NL80211_MAX_SCS_DESC)
+ return -EINVAL;
+
+ desc = kcalloc(n_desc, sizeof(*desc), GFP_KERNEL);
+ if (!desc)
+ return -ENOMEM;
+
+ res = kcalloc(n_desc, sizeof(*res), GFP_KERNEL);
+ if (!res) {
+ kfree(desc);
+ return -ENOMEM;
+ }
+
+ i = 0;
+ nla_for_each_nested(nla, info->attrs[NL80211_ATTR_SCS_DESCRIPTORS],
+ rem) {
+ desc[i] = nl80211_parse_scs_desc(nla, &res[i], info);
+ if (IS_ERR(desc[i])) {
+ err = PTR_ERR(desc[i]);
+ desc[i] = NULL;
+ goto out;
+ }
+
+ i++;
+ }
+
+ /* Pass only the descriptors that were not declined to the driver */
+ for (i = 0; i < n_desc; i++) {
+ if (res[i].status != WLAN_STATUS_SUCCESS)
+ continue;
+
+ swap(desc[i], desc[n_apply]);
+ swap(res[i], res[n_apply]);
+ n_apply++;
+ }
+
+ if (n_apply) {
+ err = rdev_set_scs(rdev, dev,
+ nla_data(info->attrs[NL80211_ATTR_MAC]),
+ desc, res, n_apply);
+ if (err)
+ goto out;
+ }
+
+ err = nl80211_set_scs_reply(info, desc, res, n_desc);
+
+out:
+ kfree(res);
+ nl80211_free_scs_desc(desc, n_desc);
+
+ return err;
+}
+
+static int nl80211_send_status_code(struct genl_info *info, u32 cmd, u16 status)
+{
+ struct sk_buff *msg;
+ void *hdr;
+
+ msg = nlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL);
+ if (!msg)
+ return -ENOMEM;
+
+ hdr = nl80211hdr_put(msg, info->snd_portid, info->snd_seq, 0, cmd);
+ if (!hdr || nla_put_u16(msg, NL80211_ATTR_STATUS_CODE, status)) {
+ nlmsg_free(msg);
+ return -ENOBUFS;
+ }
+
+ genlmsg_end(msg, hdr);
+
+ return genlmsg_reply(msg, info);
+}
+
+static int nl80211_set_mscs(struct sk_buff *skb, struct genl_info *info)
+{
+ struct cfg80211_registered_device *rdev = info->user_ptr[0];
+ struct nlattr *tb[NL80211_MSCS_DESC_ATTR_MAX + 1];
+ struct net_device *dev = info->user_ptr[1];
+ struct cfg80211_mscs_desc desc = {};
+ u16 status = WLAN_STATUS_SUCCESS;
+ struct nlattr *mask;
+ bool remove;
+ int err;
+
+ if (!wiphy_ext_feature_isset(&rdev->wiphy, NL80211_EXT_FEATURE_MSCS))
+ return -EOPNOTSUPP;
+
+ if (!info->attrs[NL80211_ATTR_MAC] ||
+ !info->attrs[NL80211_ATTR_MSCS_DESCRIPTOR])
+ return -EINVAL;
+
+ if (info->attrs[NL80211_ATTR_MLO_LINK_ID]) {
+ GENL_SET_ERR_MSG(info,
+ "an MSCS belongs to the MLD, not to a link");
+ return -EINVAL;
+ }
+
+ err = nl80211_mscs_iftype_ok(dev);
+ if (err)
+ return err;
+
+ err = nla_parse_nested(tb, NL80211_MSCS_DESC_ATTR_MAX,
+ info->attrs[NL80211_ATTR_MSCS_DESCRIPTOR],
+ nl80211_mscs_desc_policy, info->extack);
+ if (err)
+ return err;
+
+ if (!tb[NL80211_MSCS_DESC_ATTR_REQ_TYPE]) {
+ GENL_SET_ERR_MSG(info,
+ "MSCS descriptor without a request type");
+ return -EINVAL;
+ }
+
+ desc.req_type = nla_get_u8(tb[NL80211_MSCS_DESC_ATTR_REQ_TYPE]);
+ remove = desc.req_type == NL80211_SCS_REQ_REMOVE;
+
+ if (remove == !!tb[NL80211_MSCS_DESC_ATTR_TCLAS_MASK]) {
+ GENL_SET_ERR_MSG(info,
+ "TCLAS Mask does not match the request type");
+ return -EINVAL;
+ }
+
+ desc.up_bitmap =
+ nla_get_u8_default(tb[NL80211_MSCS_DESC_ATTR_UP_BITMAP], 0);
+ desc.up_limit = nla_get_u8_default(tb[NL80211_MSCS_DESC_ATTR_UP_LIMIT],
+ 7);
+ desc.stream_timeout =
+ nla_get_u32_default(tb[NL80211_MSCS_DESC_ATTR_STREAM_TIMEOUT],
+ 0);
+
+ mask = tb[NL80211_MSCS_DESC_ATTR_TCLAS_MASK];
+ if (mask) {
+ err = cfg80211_parse_tclas_mask(nla_data(mask), nla_len(mask),
+ &desc.fields);
+ if (err) {
+ status = nl80211_scs_status(err);
+ goto reply;
+ }
+ }
+
+ if (!cfg80211_mscs_desc_valid(&desc)) {
+ status = WLAN_STATUS_REQUEST_DECLINED;
+ goto reply;
+ }
+
+ err = rdev_set_mscs(rdev, dev,
+ nla_data(info->attrs[NL80211_ATTR_MAC]), &desc);
+ if (err)
+ return err;
+
+ if (remove)
+ status = WLAN_STATUS_TCLAS_PROCESSING_TERMINATED;
+
+reply:
+ return nl80211_send_status_code(info, NL80211_CMD_SET_MSCS, status);
+}
+
static int nl80211_add_tx_ts(struct sk_buff *skb, struct genl_info *info)
{
struct cfg80211_registered_device *rdev = info->user_ptr[0];
@@ -20446,6 +20859,18 @@ static const struct genl_small_ops nl80211_small_ops[] = {
.flags = GENL_UNS_ADMIN_PERM,
.internal_flags = IFLAGS(NL80211_FLAG_NEED_NETDEV_UP),
},
+ {
+ .cmd = NL80211_CMD_SET_SCS,
+ .doit = nl80211_set_scs,
+ .flags = GENL_UNS_ADMIN_PERM,
+ .internal_flags = IFLAGS(NL80211_FLAG_NEED_NETDEV_UP),
+ },
+ {
+ .cmd = NL80211_CMD_SET_MSCS,
+ .doit = nl80211_set_mscs,
+ .flags = GENL_UNS_ADMIN_PERM,
+ .internal_flags = IFLAGS(NL80211_FLAG_NEED_NETDEV_UP),
+ },
{
.cmd = NL80211_CMD_ADD_TX_TS,
.validate = GENL_DONT_VALIDATE_STRICT | GENL_DONT_VALIDATE_DUMP,
diff --git a/net/wireless/rdev-ops.h b/net/wireless/rdev-ops.h
index adcfd0278da32..8d5c99be74137 100644
--- a/net/wireless/rdev-ops.h
+++ b/net/wireless/rdev-ops.h
@@ -1188,6 +1188,38 @@ static inline int rdev_set_qos_map(struct cfg80211_registered_device *rdev,
return ret;
}
+static inline int rdev_set_scs(struct cfg80211_registered_device *rdev,
+ struct net_device *dev, const u8 *peer,
+ struct cfg80211_scs_desc * const *desc,
+ struct cfg80211_scs_result *res, u8 n_desc)
+{
+ int ret = -EOPNOTSUPP;
+
+ if (rdev->ops->set_scs) {
+ trace_rdev_set_scs(&rdev->wiphy, dev, peer, n_desc);
+ ret = rdev->ops->set_scs(&rdev->wiphy, dev, peer, desc, res,
+ n_desc);
+ trace_rdev_return_int(&rdev->wiphy, ret);
+ }
+
+ return ret;
+}
+
+static inline int rdev_set_mscs(struct cfg80211_registered_device *rdev,
+ struct net_device *dev, const u8 *peer,
+ struct cfg80211_mscs_desc *desc)
+{
+ int ret = -EOPNOTSUPP;
+
+ if (rdev->ops->set_mscs) {
+ trace_rdev_set_mscs(&rdev->wiphy, dev, peer, desc);
+ ret = rdev->ops->set_mscs(&rdev->wiphy, dev, peer, desc);
+ trace_rdev_return_int(&rdev->wiphy, ret);
+ }
+
+ return ret;
+}
+
static inline int
rdev_set_ap_chanwidth(struct cfg80211_registered_device *rdev,
struct net_device *dev,
diff --git a/net/wireless/scs.c b/net/wireless/scs.c
new file mode 100644
index 0000000000000..22c0b9d6930e2
--- /dev/null
+++ b/net/wireless/scs.c
@@ -0,0 +1,982 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Stream classification service (SCS) and mirrored SCS (MSCS)
+ *
+ * Copyright (C) 2026 Felix Fietkau <nbd@nbd.name>
+ */
+#include <kunit/visibility.h>
+#include <linux/ieee80211.h>
+#include <linux/if_vlan.h>
+#include <linux/ip.h>
+#include <linux/ipv6.h>
+#include <linux/unaligned.h>
+#include <net/cfg80211.h>
+#include <net/dsfield.h>
+#include <net/ip.h>
+#include <net/ipv6.h>
+#include "core.h"
+
+enum cfg80211_tclas_type {
+ CFG80211_TCLAS_ETH = 0,
+ CFG80211_TCLAS_IP = 4,
+ CFG80211_TCLAS_VLAN = 5,
+ CFG80211_TCLAS_FILTER = 10,
+};
+
+struct tclas_fc {
+ u8 type;
+ u8 mask;
+} __packed;
+
+struct tclas_fc_eth {
+ struct tclas_fc hdr;
+ u8 sa[ETH_ALEN];
+ u8 da[ETH_ALEN];
+ __be16 ethertype;
+} __packed;
+
+struct tclas_fc_ip4 {
+ struct tclas_fc hdr;
+ u8 version;
+ __be32 src;
+ __be32 dst;
+ __be16 src_port;
+ __be16 dst_port;
+ u8 dscp;
+ u8 proto;
+ u8 reserved;
+} __packed;
+
+struct tclas_fc_ip6 {
+ struct tclas_fc hdr;
+ u8 version;
+ struct in6_addr src;
+ struct in6_addr dst;
+ __be16 src_port;
+ __be16 dst_port;
+ u8 dscp;
+ u8 proto;
+ u8 flow_label[3];
+} __packed;
+
+struct tclas_fc_vlan {
+ struct tclas_fc hdr;
+ u8 pcp;
+ u8 dei;
+ __be16 vid;
+} __packed;
+
+/* Hashed and compared as a byte string, so it must contain no padding */
+static_assert(sizeof(struct cfg80211_flow_key) ==
+ 2 * sizeof(struct in6_addr) + sizeof(__be32) + 2 * ETH_ALEN +
+ 4 * sizeof(__be16) + 4);
+
+#define FLOW_F(name) BIT(CFG80211_FLOW_F_##name)
+
+#define FLOW_F_VLAN (FLOW_F(VLAN_PCP) | FLOW_F(VLAN_DEI) | FLOW_F(VLAN_VID))
+
+/* Reserved classifier mask bits above the array length are ignored */
+static const u8 tclas_map_eth[] = {
+ CFG80211_FLOW_F_ETH_SA,
+ CFG80211_FLOW_F_ETH_DA,
+ CFG80211_FLOW_F_ETH_TYPE,
+};
+
+/* Shared by both IP versions, only IPv6 has the flow label */
+static const u8 tclas_map_ip[] = {
+ CFG80211_FLOW_F_IP_VERSION,
+ CFG80211_FLOW_F_IP_SRC,
+ CFG80211_FLOW_F_IP_DST,
+ CFG80211_FLOW_F_SRC_PORT,
+ CFG80211_FLOW_F_DST_PORT,
+ CFG80211_FLOW_F_DSCP,
+ CFG80211_FLOW_F_PROTO,
+ CFG80211_FLOW_F_FLOW_LABEL,
+};
+
+static const u8 tclas_map_vlan[] = {
+ CFG80211_FLOW_F_VLAN_PCP,
+ CFG80211_FLOW_F_VLAN_DEI,
+ CFG80211_FLOW_F_VLAN_VID,
+};
+
+static u32 tclas_mask_fields(u8 type, u8 version, u8 mask)
+{
+ size_t n = ARRAY_SIZE(tclas_map_ip);
+ const u8 *map = tclas_map_ip;
+ unsigned int i;
+ u32 fields = 0;
+
+ switch (type) {
+ case CFG80211_TCLAS_ETH:
+ map = tclas_map_eth;
+ n = ARRAY_SIZE(tclas_map_eth);
+ break;
+ case CFG80211_TCLAS_VLAN:
+ map = tclas_map_vlan;
+ n = ARRAY_SIZE(tclas_map_vlan);
+ break;
+ default:
+ n -= version == 4;
+ break;
+ }
+
+ for (i = 0; i < n; i++)
+ if (mask & BIT(i))
+ fields |= BIT(map[i]);
+
+ return fields;
+}
+
+static void flow_key_select(const struct cfg80211_flow_key *src, u32 fields,
+ struct cfg80211_flow_key *dst)
+{
+ memset(dst, 0, sizeof(*dst));
+
+ if (fields & FLOW_F(ETH_SA))
+ memcpy(dst->sa, src->sa, ETH_ALEN);
+ if (fields & FLOW_F(ETH_DA))
+ memcpy(dst->da, src->da, ETH_ALEN);
+ if (fields & FLOW_F(ETH_TYPE))
+ dst->eth_type = src->eth_type;
+
+ if (fields & FLOW_F_VLAN) {
+ u16 tci = 0;
+
+ if (fields & FLOW_F(VLAN_PCP))
+ tci |= VLAN_PRIO_MASK;
+ if (fields & FLOW_F(VLAN_DEI))
+ tci |= VLAN_CFI_MASK;
+ if (fields & FLOW_F(VLAN_VID))
+ tci |= VLAN_VID_MASK;
+
+ dst->vlan_tci = src->vlan_tci & htons(tci);
+ }
+
+ if (fields & FLOW_F(IP_VERSION))
+ dst->ip_version = src->ip_version;
+ if (fields & FLOW_F(IP_SRC))
+ dst->src = src->src;
+ if (fields & FLOW_F(IP_DST))
+ dst->dst = src->dst;
+ if (fields & FLOW_F(SRC_PORT))
+ dst->src_port = src->src_port;
+ if (fields & FLOW_F(DST_PORT))
+ dst->dst_port = src->dst_port;
+
+ if (fields & FLOW_F(DSCP))
+ dst->dscp = src->dscp;
+ if (fields & FLOW_F(PROTO))
+ dst->proto = src->proto;
+ if (fields & FLOW_F(FLOW_LABEL))
+ dst->flow_label = src->flow_label;
+}
+
+static int tclas_parse_eth(struct cfg80211_flow_key *key, const u8 *data,
+ size_t len)
+{
+ const struct tclas_fc_eth *fc = (const void *)data;
+
+ if (len != sizeof(*fc))
+ return -EINVAL;
+
+ memcpy(key->sa, fc->sa, ETH_ALEN);
+ memcpy(key->da, fc->da, ETH_ALEN);
+ key->eth_type = fc->ethertype;
+
+ return 0;
+}
+
+static int tclas_parse_ip4(struct cfg80211_flow_key *key,
+ const struct tclas_fc_ip4 *fc, bool values)
+{
+ if (values && fc->version != 4)
+ return -EINVAL;
+
+ key->ip_version = 4;
+ memcpy(&key->src, &fc->src, sizeof(fc->src));
+ memcpy(&key->dst, &fc->dst, sizeof(fc->dst));
+ key->src_port = fc->src_port;
+ key->dst_port = fc->dst_port;
+ key->dscp = fc->dscp & 0x3f;
+ key->proto = fc->proto;
+
+ return 0;
+}
+
+static int tclas_parse_ip6(struct cfg80211_flow_key *key,
+ const struct tclas_fc_ip6 *fc, bool values)
+{
+ if (values && fc->version != 6)
+ return -EINVAL;
+
+ key->ip_version = 6;
+ memcpy(&key->src, &fc->src, sizeof(fc->src));
+ memcpy(&key->dst, &fc->dst, sizeof(fc->dst));
+ key->src_port = fc->src_port;
+ key->dst_port = fc->dst_port;
+ key->dscp = fc->dscp & 0x3f;
+ key->proto = fc->proto;
+ /* Same encoding as ip6_flowlabel(), so the two compare equal */
+ key->flow_label =
+ cpu_to_be32(get_unaligned_be24(fc->flow_label) & 0xfffff);
+
+ return 0;
+}
+
+/* The Version subfield is reserved in a mask, so the length selects v4/v6 */
+static int tclas_parse_ip(struct cfg80211_flow_key *key, const u8 *data,
+ size_t len, bool values)
+{
+ if (len == sizeof(struct tclas_fc_ip4))
+ return tclas_parse_ip4(key, (const void *)data, values);
+
+ if (len == sizeof(struct tclas_fc_ip6))
+ return tclas_parse_ip6(key, (const void *)data, values);
+
+ return -EINVAL;
+}
+
+static int tclas_parse_vlan(struct cfg80211_flow_key *key, const u8 *data,
+ size_t len)
+{
+ const struct tclas_fc_vlan *fc = (const void *)data;
+ u16 tci;
+
+ if (len != sizeof(*fc))
+ return -EINVAL;
+
+ if (fc->pcp > 7 || fc->dei > 1 || be16_to_cpu(fc->vid) > 0xfff)
+ return -EINVAL;
+
+ tci = fc->pcp << 13;
+ tci |= fc->dei << 12;
+ tci |= be16_to_cpu(fc->vid);
+ key->vlan_tci = cpu_to_be16(tci);
+
+ return 0;
+}
+
+/* The octets after the header are the value and the mask, of equal length */
+static int tclas_parse_filter(struct cfg80211_tclas *t, const u8 *data,
+ size_t len)
+{
+ struct cfg80211_tclas_filter *f = &t->filter;
+
+ if (len < 3 || (len - 3) % 2)
+ return -EINVAL;
+
+ f->len = (len - 3) / 2;
+ if (!f->len || f->len > CFG80211_TCLAS_FILTER_LEN)
+ return -EOPNOTSUPP;
+
+ f->instance = data[1];
+ f->protocol = data[2];
+ memcpy(f->value, data + 3, f->len);
+ memcpy(f->mask, data + 3 + f->len, f->len);
+
+ return 0;
+}
+
+/* @values is false for a TCLAS Mask element, which carries no values */
+static int cfg80211_parse_frame_classifier(struct cfg80211_tclas *t,
+ const u8 *data, size_t len,
+ bool values)
+{
+ const struct tclas_fc *fc = (const void *)data;
+ struct cfg80211_flow_key raw = {};
+ int ret;
+
+ if (len < sizeof(*fc))
+ return -EINVAL;
+
+ memset(t, 0, sizeof(*t));
+ t->type = fc->type;
+
+ switch (t->type) {
+ case CFG80211_TCLAS_ETH:
+ ret = tclas_parse_eth(&raw, data, len);
+ break;
+ case CFG80211_TCLAS_IP:
+ ret = tclas_parse_ip(&raw, data, len, values);
+ break;
+ case CFG80211_TCLAS_VLAN:
+ ret = tclas_parse_vlan(&raw, data, len);
+ break;
+ case CFG80211_TCLAS_FILTER:
+ /* A mask element selects fields, which a filter does not */
+ if (!values)
+ return -EOPNOTSUPP;
+
+ return tclas_parse_filter(t, data, len);
+ default:
+ return -EOPNOTSUPP;
+ }
+
+ if (ret)
+ return ret;
+
+ t->fields = tclas_mask_fields(t->type, raw.ip_version, fc->mask);
+
+ /* A mask that selects nothing would match every MSDU */
+ if (!t->fields)
+ return -EINVAL;
+
+ /* Always match on the IP version, which the element length selects */
+ if (t->type == CFG80211_TCLAS_IP)
+ t->fields |= FLOW_F(IP_VERSION);
+
+ flow_key_select(&raw, t->fields, &t->key);
+
+ return 0;
+}
+
+/**
+ * cfg80211_tclas_count - count the TCLAS elements of a chain
+ *
+ * @elems: element chain, as it arrived over the air
+ * @len: length of @elems
+ *
+ * Sizes the array for cfg80211_parse_tclas(). Does not validate the
+ * elements.
+ *
+ * Return: the number of TCLAS elements, or -ENOSPC for more than a descriptor
+ * can hold.
+ */
+int cfg80211_tclas_count(const u8 *elems, size_t len)
+{
+ const struct element *elem;
+ unsigned int n = 0;
+
+ for_each_element_id(elem, WLAN_EID_TCLAS, elems, len)
+ n++;
+
+ if (n > U8_MAX)
+ return -ENOSPC;
+
+ return n;
+}
+EXPORT_SYMBOL_IF_CFG80211_KUNIT(cfg80211_tclas_count);
+
+/**
+ * cfg80211_parse_tclas - parse a chain of TCLAS elements
+ *
+ * @elems: element chain, as it arrived over the air
+ * @len: length of @elems
+ * @out: array that receives the parsed elements
+ * @n_tclas: number of entries in @out, from cfg80211_tclas_count()
+ * @processing: receives how the elements relate, or
+ * %CFG80211_TCLAS_PROCESSING_ABSENT when the chain carries no TCLAS
+ * Processing element
+ *
+ * The chain holds TCLAS elements and at most one TCLAS Processing element.
+ *
+ * Return: 0 on success, -EINVAL for a malformed chain, -EOPNOTSUPP for a
+ * classifier the kernel cannot evaluate, -ENOSPC for a chain that no
+ * longer fits @out.
+ */
+int cfg80211_parse_tclas(const u8 *elems, size_t len,
+ struct cfg80211_tclas *out, u8 n_tclas,
+ enum cfg80211_tclas_processing *processing)
+{
+ const struct element *elem;
+ unsigned int n = 0;
+ int ret;
+
+ *processing = CFG80211_TCLAS_PROCESSING_ABSENT;
+
+ for_each_element(elem, elems, len) {
+ switch (elem->id) {
+ case WLAN_EID_TCLAS:
+ if (n == n_tclas)
+ return -ENOSPC;
+
+ if (elem->datalen < 1)
+ return -EINVAL;
+
+ /*
+ * Any other value matches on the UP of the MSDU,
+ * which is not known yet at classification time.
+ */
+ if (elem->data[0] != 255)
+ return -EOPNOTSUPP;
+
+ ret = cfg80211_parse_frame_classifier(&out[n],
+ elem->data + 1,
+ elem->datalen - 1,
+ true);
+ if (ret)
+ return ret;
+
+ n++;
+ break;
+ case WLAN_EID_TCLAS_PROCESSING:
+ if (elem->datalen != 1 ||
+ *processing != CFG80211_TCLAS_PROCESSING_ABSENT ||
+ elem->data[0] > CFG80211_TCLAS_PROCESSING_DEFAULT)
+ return -EINVAL;
+
+ *processing = elem->data[0];
+ break;
+ default:
+ return -EINVAL;
+ }
+ }
+
+ if (!for_each_element_completed(elem, elems, len))
+ return -EINVAL;
+
+ return 0;
+}
+EXPORT_SYMBOL_IF_CFG80211_KUNIT(cfg80211_parse_tclas);
+
+/**
+ * cfg80211_parse_tclas_mask - parse a chain of TCLAS Mask elements
+ *
+ * @elems: element chain, as it arrived over the air
+ * @len: length of @elems
+ * @fields: receives the union of the selected classifier parameters, as a
+ * bitmap of &enum cfg80211_flow_field
+ *
+ * A TCLAS Mask element carries no values, only the field selection.
+ *
+ * Return: 0 on success, -EINVAL for a malformed chain, -EOPNOTSUPP for a
+ * classifier type the kernel cannot evaluate.
+ */
+int cfg80211_parse_tclas_mask(const u8 *elems, size_t len, u32 *fields)
+{
+ const struct element *elem;
+ struct cfg80211_tclas t;
+ int ret;
+
+ *fields = 0;
+
+ for_each_element(elem, elems, len) {
+ if (elem->id != WLAN_EID_EXTENSION || elem->datalen < 1 ||
+ elem->data[0] != WLAN_EID_EXT_TCLAS_MASK)
+ return -EINVAL;
+
+ ret = cfg80211_parse_frame_classifier(&t, elem->data + 1,
+ elem->datalen - 1, false);
+ if (ret)
+ return ret;
+
+ *fields |= t.fields;
+ }
+
+ if (!for_each_element_completed(elem, elems, len))
+ return -EINVAL;
+
+ return 0;
+}
+EXPORT_SYMBOL_IF_CFG80211_KUNIT(cfg80211_parse_tclas_mask);
+
+#define FLOW_F_PORTS (FLOW_F(SRC_PORT) | FLOW_F(DST_PORT))
+
+static void cfg80211_flow_parse_ports(struct sk_buff *skb,
+ unsigned int offset, int proto,
+ struct cfg80211_flow_info *info)
+{
+ __be16 ports[2], *p;
+
+ switch (proto) {
+ case IPPROTO_TCP:
+ case IPPROTO_UDP:
+ case IPPROTO_SCTP:
+ break;
+ default:
+ return;
+ }
+
+ p = skb_header_pointer(skb, offset, sizeof(ports), ports);
+ if (!p)
+ return;
+
+ info->key.src_port = p[0];
+ info->key.dst_port = p[1];
+ info->present |= FLOW_F_PORTS;
+}
+
+static void cfg80211_flow_parse_ipv4(struct sk_buff *skb, unsigned int offset,
+ struct cfg80211_flow_info *info)
+{
+ struct iphdr iph_buf, *iph;
+
+ iph = skb_header_pointer(skb, offset, sizeof(*iph), &iph_buf);
+ if (!iph || iph->version != 4 || iph->ihl < 5)
+ return;
+
+ info->key.ip_version = 4;
+ info->key.dscp = ipv4_get_dsfield(iph) >> 2;
+ info->key.proto = iph->protocol;
+ memcpy(&info->key.src, &iph->saddr, sizeof(iph->saddr));
+ memcpy(&info->key.dst, &iph->daddr, sizeof(iph->daddr));
+ info->present |= FLOW_F(IP_VERSION) | FLOW_F(IP_SRC) | FLOW_F(IP_DST) |
+ FLOW_F(DSCP) | FLOW_F(PROTO);
+
+ if (iph->frag_off & htons(IP_OFFSET))
+ return;
+
+ cfg80211_flow_parse_ports(skb, offset + iph->ihl * 4, iph->protocol,
+ info);
+}
+
+static void cfg80211_flow_parse_ipv6(struct sk_buff *skb, unsigned int offset,
+ struct cfg80211_flow_info *info)
+{
+ struct ipv6hdr ip6h_buf, *ip6h;
+ __be16 frag_off = 0;
+ int l4_off;
+ u8 nexthdr;
+
+ ip6h = skb_header_pointer(skb, offset, sizeof(*ip6h), &ip6h_buf);
+ if (!ip6h || ip6h->version != 6)
+ return;
+
+ info->key.ip_version = 6;
+ info->key.dscp = ipv6_get_dsfield(ip6h) >> 2;
+ info->key.flow_label = ip6_flowlabel(ip6h);
+ info->key.src = ip6h->saddr;
+ info->key.dst = ip6h->daddr;
+
+ /* The classifier matches Next Header, not the layer 4 protocol */
+ info->key.proto = ip6h->nexthdr;
+ info->present |= FLOW_F(IP_VERSION) | FLOW_F(IP_SRC) | FLOW_F(IP_DST) |
+ FLOW_F(DSCP) | FLOW_F(FLOW_LABEL) | FLOW_F(PROTO);
+
+ nexthdr = ip6h->nexthdr;
+ l4_off = ipv6_skip_exthdr(skb, offset + sizeof(*ip6h), &nexthdr,
+ &frag_off);
+ if (l4_off < 0 || frag_off & htons(IP6_OFFSET))
+ return;
+
+ cfg80211_flow_parse_ports(skb, l4_off, nexthdr, info);
+}
+
+/**
+ * cfg80211_flow_parse - read the classifier parameters of an MSDU
+ *
+ * @skb: the frame, in IEEE 802.3 format
+ * @info: receives the parameters
+ *
+ * Parses from skb->data, since a frame forwarded between two stations
+ * arrives with skb->protocol set to ETH_P_802_3 and no network header.
+ *
+ * Return: %true when @info describes the frame.
+ */
+bool cfg80211_flow_parse(struct sk_buff *skb, struct cfg80211_flow_info *info)
+{
+ struct ethhdr eth_buf, *eth;
+ unsigned int offset = ETH_HLEN;
+ __be16 proto;
+
+ memset(info, 0, sizeof(*info));
+
+ eth = skb_header_pointer(skb, 0, sizeof(*eth), ð_buf);
+ if (!eth)
+ return false;
+
+ ether_addr_copy(info->key.sa, eth->h_source);
+ ether_addr_copy(info->key.da, eth->h_dest);
+ info->present = FLOW_F(ETH_SA) | FLOW_F(ETH_DA);
+ proto = eth->h_proto;
+
+ if (skb_vlan_tag_present(skb)) {
+ info->key.vlan_tci = htons(skb_vlan_tag_get(skb));
+ info->present |= FLOW_F_VLAN;
+ } else if (eth_type_vlan(proto)) {
+ struct vlan_hdr vhdr_buf, *vhdr;
+
+ vhdr = skb_header_pointer(skb, offset, sizeof(*vhdr),
+ &vhdr_buf);
+ if (!vhdr)
+ return false;
+
+ info->key.vlan_tci = vhdr->h_vlan_TCI;
+ info->present |= FLOW_F_VLAN;
+ proto = vhdr->h_vlan_encapsulated_proto;
+ offset += sizeof(*vhdr);
+ }
+
+ /* Use the type behind the VLAN tag, whether it was stripped or not */
+ info->key.eth_type = proto;
+ info->present |= FLOW_F(ETH_TYPE);
+
+ info->skb = skb;
+ info->l3_off = offset;
+
+ switch (proto) {
+ case htons(ETH_P_IP):
+ cfg80211_flow_parse_ipv4(skb, offset, info);
+ break;
+ case htons(ETH_P_IPV6):
+ cfg80211_flow_parse_ipv6(skb, offset, info);
+ break;
+ }
+
+ return true;
+}
+EXPORT_SYMBOL(cfg80211_flow_parse);
+
+static void flow_key_swap(struct cfg80211_flow_key *k)
+{
+ u8 addr[ETH_ALEN];
+
+ memcpy(addr, k->sa, ETH_ALEN);
+ memcpy(k->sa, k->da, ETH_ALEN);
+ memcpy(k->da, addr, ETH_ALEN);
+
+ swap(k->src, k->dst);
+ swap(k->src_port, k->dst_port);
+}
+
+static u32 cfg80211_flow_fields_mirror(u32 fields)
+{
+ static const u8 pairs[][2] = {
+ { CFG80211_FLOW_F_ETH_SA, CFG80211_FLOW_F_ETH_DA },
+ { CFG80211_FLOW_F_IP_SRC, CFG80211_FLOW_F_IP_DST },
+ { CFG80211_FLOW_F_SRC_PORT, CFG80211_FLOW_F_DST_PORT },
+ };
+ unsigned int i;
+ u32 out = fields;
+
+ for (i = 0; i < ARRAY_SIZE(pairs); i++) {
+ u32 lo = BIT(pairs[i][0]), hi = BIT(pairs[i][1]);
+
+ out &= ~(lo | hi);
+ if (fields & lo)
+ out |= hi;
+ if (fields & hi)
+ out |= lo;
+ }
+
+ return out;
+}
+
+/**
+ * cfg80211_flow_key_build - build an MSCS lookup key
+ *
+ * @info: parsed frame, from cfg80211_flow_parse()
+ * @fields: classifier parameters of the MSCS, a bitmap of
+ * &enum cfg80211_flow_field
+ * @dir: %CFG80211_FLOW_MIRRORED when @info describes an uplink frame and the
+ * key must describe the downlink direction
+ * @key: receives the key
+ *
+ * Everything that @fields does not select stays zero, so the key is hashed and
+ * compared as a byte string.
+ *
+ * Return: %false when @info lacks a parameter that @fields selects, in which
+ * case the MSDU is not classified at all.
+ */
+bool cfg80211_flow_key_build(const struct cfg80211_flow_info *info, u32 fields,
+ enum cfg80211_flow_dir dir,
+ struct cfg80211_flow_key *key)
+{
+ struct cfg80211_flow_key mirrored;
+
+ if (dir == CFG80211_FLOW_AS_IS) {
+ if (fields & ~info->present)
+ return false;
+
+ flow_key_select(&info->key, fields, key);
+
+ return true;
+ }
+
+ if (cfg80211_flow_fields_mirror(fields) & ~info->present)
+ return false;
+
+ mirrored = info->key;
+ flow_key_swap(&mirrored);
+ flow_key_select(&mirrored, fields, key);
+
+ return true;
+}
+EXPORT_SYMBOL(cfg80211_flow_key_build);
+
+/* Find the payload behind the @instance-th occurrence of the protocol */
+static bool tclas_filter_offset(const struct cfg80211_flow_info *info,
+ const struct cfg80211_tclas_filter *f,
+ unsigned int *off)
+{
+ struct ipv6hdr ip6h_buf, *ip6h;
+ unsigned int pos = info->l3_off;
+ unsigned int hdrs = 0;
+ u8 instance = 0;
+ u8 nexthdr;
+
+ if (info->key.ip_version == 4) {
+ struct iphdr iph_buf, *iph;
+
+ iph = skb_header_pointer(info->skb, pos, sizeof(*iph),
+ &iph_buf);
+ if (!iph || iph->protocol != f->protocol || f->instance)
+ return false;
+
+ *off = pos + iph->ihl * 4;
+
+ return true;
+ }
+
+ if (info->key.ip_version != 6)
+ return false;
+
+ ip6h = skb_header_pointer(info->skb, pos, sizeof(*ip6h), &ip6h_buf);
+ if (!ip6h)
+ return false;
+
+ nexthdr = ip6h->nexthdr;
+ pos += sizeof(*ip6h);
+
+ while (hdrs++ < 8) {
+ struct ipv6_opt_hdr hdr_buf, *hdr;
+
+ if (nexthdr == f->protocol && instance++ == f->instance) {
+ *off = pos;
+
+ return true;
+ }
+
+ if (!ipv6_ext_hdr(nexthdr) || nexthdr == NEXTHDR_NONE)
+ return false;
+
+ hdr = skb_header_pointer(info->skb, pos, sizeof(*hdr),
+ &hdr_buf);
+ if (!hdr)
+ return false;
+
+ if (nexthdr == NEXTHDR_FRAGMENT)
+ pos += 8;
+ else if (nexthdr == NEXTHDR_AUTH)
+ pos += ipv6_authlen(hdr);
+ else
+ pos += ipv6_optlen(hdr);
+
+ nexthdr = hdr->nexthdr;
+ }
+
+ return false;
+}
+
+static bool tclas_filter_match(const struct cfg80211_tclas *t,
+ const struct cfg80211_flow_info *info)
+{
+ const struct cfg80211_tclas_filter *f = &t->filter;
+ u8 buf[CFG80211_TCLAS_FILTER_LEN], *p;
+ unsigned int i, off;
+
+ if (!info->skb)
+ return false;
+
+ if (!tclas_filter_offset(info, f, &off))
+ return false;
+
+ p = skb_header_pointer(info->skb, off, f->len, buf);
+ if (!p)
+ return false;
+
+ for (i = 0; i < f->len; i++)
+ if ((p[i] ^ f->value[i]) & f->mask[i])
+ return false;
+
+ return true;
+}
+
+static bool tclas_match(const struct cfg80211_tclas *t,
+ const struct cfg80211_flow_info *info)
+{
+ struct cfg80211_flow_key key;
+
+ if (t->type == CFG80211_TCLAS_FILTER)
+ return tclas_filter_match(t, info);
+
+ if (!cfg80211_flow_key_build(info, t->fields, CFG80211_FLOW_AS_IS,
+ &key))
+ return false;
+
+ return !memcmp(&key, &t->key, sizeof(key));
+}
+
+/* Number of classifier parameters required for a match, see 11.25.2 */
+static u16 tclas_param_count(const struct cfg80211_tclas *t)
+{
+ if (t->type == CFG80211_TCLAS_FILTER)
+ return 1;
+
+ if (t->type == CFG80211_TCLAS_VLAN)
+ return 3;
+
+ return hweight32(t->fields & ~FLOW_F(IP_VERSION));
+}
+
+static bool scs_desc_match(const struct cfg80211_scs_desc *desc,
+ const struct cfg80211_flow_info *info)
+{
+ unsigned int i;
+ bool any = false;
+
+ for (i = 0; i < desc->n_tclas; i++) {
+ if (tclas_match(&desc->tclas[i], info)) {
+ any = true;
+ continue;
+ }
+
+ if (desc->tclas_processing == CFG80211_TCLAS_PROCESSING_ALL)
+ return false;
+ }
+
+ return any;
+}
+
+static u16 scs_desc_param_count(const struct cfg80211_scs_desc *desc)
+{
+ unsigned int i;
+ u16 count;
+
+ if (desc->tclas_processing != CFG80211_TCLAS_PROCESSING_ALL) {
+ count = U16_MAX;
+ for (i = 0; i < desc->n_tclas; i++)
+ count = min(count, tclas_param_count(&desc->tclas[i]));
+
+ return count == U16_MAX ? 0 : count;
+ }
+
+ count = 0;
+ for (i = 0; i < desc->n_tclas; i++)
+ count += tclas_param_count(&desc->tclas[i]);
+
+ return count;
+}
+
+/**
+ * cfg80211_scs_evaluate - pick the SCS descriptor that matches a flow
+ *
+ * @desc: the active descriptors of one peer
+ * @n_desc: number of entries in @desc
+ * @info: the flow, from cfg80211_flow_parse()
+ * @verdict: receives the result
+ *
+ * When several descriptors match, the one with the most classifier parameters
+ * wins. An equal count keeps the first.
+ */
+void cfg80211_scs_evaluate(struct cfg80211_scs_desc * const *desc, u8 n_desc,
+ const struct cfg80211_flow_info *info,
+ struct cfg80211_scs_verdict *verdict)
+{
+ unsigned int i;
+ int best = -1;
+
+ memset(verdict, 0, sizeof(*verdict));
+
+ for (i = 0; i < n_desc; i++) {
+ u16 count;
+
+ if (!scs_desc_match(desc[i], info))
+ continue;
+
+ count = scs_desc_param_count(desc[i]);
+ if (count <= best)
+ continue;
+
+ best = count;
+ verdict->match = true;
+ verdict->scsid = desc[i]->id;
+ verdict->up = desc[i]->up;
+ }
+}
+EXPORT_SYMBOL(cfg80211_scs_evaluate);
+
+/* Two elements of a match-all descriptor can require conflicting values */
+static bool tclas_conflict(const struct cfg80211_tclas *a,
+ const struct cfg80211_tclas *b)
+{
+ struct cfg80211_flow_key ka, kb;
+ u32 common = a->fields & b->fields;
+
+ if (!common)
+ return false;
+
+ flow_key_select(&a->key, common, &ka);
+ flow_key_select(&b->key, common, &kb);
+
+ return memcmp(&ka, &kb, sizeof(ka));
+}
+
+/**
+ * cfg80211_scs_desc_valid - validate one SCS descriptor
+ *
+ * @desc: the descriptor, already parsed
+ *
+ * Checks the constraints between the request type, the classifier and the
+ * traffic description. cfg80211_parse_tclas() validates each element.
+ *
+ * Return: whether the descriptor may be installed. The caller reports a
+ * refusal as a status code, not as an error.
+ */
+bool cfg80211_scs_desc_valid(const struct cfg80211_scs_desc *desc)
+{
+ bool want_tclas = true;
+ unsigned int i, j;
+
+ if (desc->req_type == NL80211_SCS_REQ_REMOVE)
+ return true;
+
+ /* Only a downlink stream is classified by the AP */
+ if (desc->qos_char &&
+ ieee80211_qos_char_direction(desc->qos_char) !=
+ IEEE80211_QOS_CHAR_DIR_DOWNLINK)
+ want_tclas = false;
+
+ if (want_tclas != !!desc->n_tclas)
+ return false;
+
+ if (!want_tclas)
+ return desc->tclas_processing ==
+ CFG80211_TCLAS_PROCESSING_ABSENT;
+
+ switch (desc->tclas_processing) {
+ case CFG80211_TCLAS_PROCESSING_ALL:
+ case CFG80211_TCLAS_PROCESSING_ANY:
+ if (desc->n_tclas < 2)
+ return false;
+ break;
+ case CFG80211_TCLAS_PROCESSING_DEFAULT:
+ return false;
+ case CFG80211_TCLAS_PROCESSING_ABSENT:
+ if (desc->n_tclas > 1)
+ return false;
+ break;
+ default:
+ return false;
+ }
+
+ if (desc->tclas_processing != CFG80211_TCLAS_PROCESSING_ALL)
+ return true;
+
+ for (i = 0; i < desc->n_tclas; i++)
+ for (j = i + 1; j < desc->n_tclas; j++)
+ if (tclas_conflict(&desc->tclas[i], &desc->tclas[j]))
+ return false;
+
+ return true;
+}
+EXPORT_SYMBOL_IF_CFG80211_KUNIT(cfg80211_scs_desc_valid);
+
+/**
+ * cfg80211_mscs_desc_valid - validate an MSCS descriptor
+ *
+ * @desc: the descriptor, already parsed
+ *
+ * Return: whether the descriptor may be installed.
+ */
+bool cfg80211_mscs_desc_valid(const struct cfg80211_mscs_desc *desc)
+{
+ bool remove = desc->req_type == NL80211_SCS_REQ_REMOVE;
+
+ if (remove == !!desc->fields)
+ return false;
+
+ /* A zero timeout would expire every entry at the next collection */
+ return remove || desc->stream_timeout;
+}
diff --git a/net/wireless/tests/Makefile b/net/wireless/tests/Makefile
index c364e63b508eb..a4b7f2e206fc2 100644
--- a/net/wireless/tests/Makefile
+++ b/net/wireless/tests/Makefile
@@ -1,3 +1,3 @@
-cfg80211-tests-y += module.o fragmentation.o scan.o util.o chan.o
+cfg80211-tests-y += module.o fragmentation.o scan.o util.o chan.o scs.o
obj-$(CONFIG_CFG80211_KUNIT_TEST) += cfg80211-tests.o
diff --git a/net/wireless/tests/scs.c b/net/wireless/tests/scs.c
new file mode 100644
index 0000000000000..d3636bd459644
--- /dev/null
+++ b/net/wireless/tests/scs.c
@@ -0,0 +1,488 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * KUnit tests for the SCS and MSCS classifier
+ *
+ * Copyright (C) 2026 Felix Fietkau <nbd@nbd.name>
+ */
+#include <kunit/skbuff.h>
+#include <kunit/test.h>
+#include <linux/ieee80211.h>
+#include <linux/ip.h>
+#include <net/cfg80211.h>
+#include "../core.h"
+
+static const u8 test_da[ETH_ALEN] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x02 };
+
+/* An Ethernet header, an IPv4 header and four octets of UDP ports */
+static const u8 udp_frame[] = {
+ 0x02, 0x00, 0x00, 0x00, 0x00, 0x02, /* destination address */
+ 0x02, 0x00, 0x00, 0x00, 0x00, 0x01, /* source address */
+ 0x08, 0x00,
+ 0x45, 0x00, 0x00, 0x20, 0xab, 0xcd, 0x00, 0x00,
+ 0x40, 0x11, 0x00, 0x00,
+ 192, 168, 1, 1,
+ 192, 168, 1, 2,
+ 0x13, 0x88, 0x27, 0x10,
+ 0xde, 0xad, 0xbe, 0xef,
+};
+
+/* The same datagram, as a fragment with a nonzero offset */
+static const u8 udp_fragment[] = {
+ 0x02, 0x00, 0x00, 0x00, 0x00, 0x02, /* destination address */
+ 0x02, 0x00, 0x00, 0x00, 0x00, 0x01, /* source address */
+ 0x08, 0x00,
+ 0x45, 0x00, 0x00, 0x20, 0xab, 0xcd, 0x00, 0x02,
+ 0x40, 0x11, 0x00, 0x00,
+ 192, 168, 1, 1,
+ 192, 168, 1, 2,
+ 0x13, 0x88, 0x27, 0x10,
+ 0xde, 0xad, 0xbe, 0xef,
+};
+
+static struct sk_buff *test_skb(struct kunit *test, const u8 *data,
+ size_t len)
+{
+ struct sk_buff *skb = kunit_zalloc_skb(test, 256, GFP_KERNEL);
+
+ KUNIT_ASSERT_NOT_NULL(test, skb);
+ skb_put_data(skb, data, len);
+
+ return skb;
+}
+
+#define TEST_SKB(test, buf) test_skb(test, buf, sizeof(buf))
+
+#define FLOW_F(name) BIT(CFG80211_FLOW_F_##name)
+
+#define QC(buf) ((const struct ieee80211_qos_char_elem *)(buf))
+#define QC_OK(buf) ieee80211_qos_char_size_ok(buf, sizeof(buf))
+
+/* A later fragment carries no ports */
+static void flow_parse_fragment(struct kunit *test)
+{
+ struct cfg80211_flow_info info;
+
+ KUNIT_ASSERT_TRUE(test,
+ cfg80211_flow_parse(TEST_SKB(test, udp_frame),
+ &info));
+ KUNIT_EXPECT_TRUE(test, info.present & FLOW_F(SRC_PORT));
+ KUNIT_EXPECT_TRUE(test, info.present & FLOW_F(DST_PORT));
+ KUNIT_EXPECT_EQ(test, be16_to_cpu(info.key.src_port), 5000);
+ KUNIT_EXPECT_EQ(test, be16_to_cpu(info.key.dst_port), 10000);
+ KUNIT_EXPECT_EQ(test, info.key.proto, IPPROTO_UDP);
+
+ KUNIT_ASSERT_TRUE(test,
+ cfg80211_flow_parse(TEST_SKB(test, udp_fragment),
+ &info));
+ KUNIT_EXPECT_FALSE(test, info.present & FLOW_F(SRC_PORT));
+ KUNIT_EXPECT_FALSE(test, info.present & FLOW_F(DST_PORT));
+ KUNIT_EXPECT_EQ(test, be16_to_cpu(info.key.src_port), 0);
+ KUNIT_EXPECT_EQ(test, be16_to_cpu(info.key.dst_port), 0);
+ KUNIT_EXPECT_TRUE(test, info.present & FLOW_F(IP_SRC));
+ KUNIT_EXPECT_TRUE(test, info.present & FLOW_F(IP_DST));
+}
+
+static struct cfg80211_scs_desc *
+test_scs_desc(struct kunit *test, u8 id, u8 up, const u8 *elems, size_t len)
+{
+ struct cfg80211_scs_desc *desc;
+ int n_tclas;
+
+ n_tclas = cfg80211_tclas_count(elems, len);
+ KUNIT_ASSERT_GE(test, n_tclas, 0);
+
+ desc = kunit_kzalloc(test, struct_size(desc, tclas, n_tclas),
+ GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, desc);
+
+ desc->id = id;
+ desc->up = up;
+ desc->n_tclas = n_tclas;
+ KUNIT_ASSERT_EQ(test, 0,
+ cfg80211_parse_tclas(elems, len, desc->tclas, n_tclas,
+ &desc->tclas_processing));
+
+ return desc;
+}
+
+/* When two descriptors match, the more granular one wins */
+static void scs_granularity(struct kunit *test)
+{
+ static const u8 two_params[] = {
+ WLAN_EID_TCLAS, 19,
+ 255,
+ 4, 0x06, /* type, mask: src and dst address */
+ 4, /* version */
+ 192, 168, 1, 1,
+ 192, 168, 1, 2,
+ 0, 0, 0, 0, /* ports, not selected */
+ 0, 0, 0, /* dscp, protocol, reserved */
+ };
+ static const u8 three_params[] = {
+ WLAN_EID_TCLAS, 19,
+ 255,
+ 4, 0x16, /* src, dst, dst port */
+ 4,
+ 192, 168, 1, 1,
+ 192, 168, 1, 2,
+ 0, 0, 0x27, 0x10,
+ 0, 0, 0,
+ };
+ static const u8 wrong_port[] = {
+ WLAN_EID_TCLAS, 19,
+ 255,
+ 4, 0x08, /* src port only */
+ 4,
+ 0, 0, 0, 0,
+ 0, 0, 0, 0,
+ 0x00, 0x35, 0, 0,
+ 0, 0, 0,
+ };
+ struct cfg80211_scs_desc *list[3];
+ struct cfg80211_scs_verdict verdict;
+ struct cfg80211_flow_info info;
+
+ list[0] = test_scs_desc(test, 1, 3, two_params, sizeof(two_params));
+ list[1] = test_scs_desc(test, 2, 6, three_params, sizeof(three_params));
+ list[2] = test_scs_desc(test, 3, 7, wrong_port, sizeof(wrong_port));
+
+ KUNIT_ASSERT_TRUE(test,
+ cfg80211_flow_parse(TEST_SKB(test, udp_frame),
+ &info));
+
+ /* The IP version is always matched, but does not count as a parameter */
+ KUNIT_EXPECT_EQ(test, list[0]->tclas[0].fields,
+ FLOW_F(IP_VERSION) | FLOW_F(IP_SRC) | FLOW_F(IP_DST));
+ KUNIT_EXPECT_EQ(test, list[1]->tclas[0].fields,
+ FLOW_F(IP_VERSION) | FLOW_F(IP_SRC) | FLOW_F(IP_DST) |
+ FLOW_F(DST_PORT));
+
+ cfg80211_scs_evaluate(list, 3, &info, &verdict);
+ KUNIT_EXPECT_TRUE(test, verdict.match);
+ KUNIT_EXPECT_EQ(test, verdict.scsid, 2);
+ KUNIT_EXPECT_EQ(test, verdict.up, 6);
+
+ swap(list[0], list[1]);
+ cfg80211_scs_evaluate(list, 3, &info, &verdict);
+ KUNIT_EXPECT_TRUE(test, verdict.match);
+ KUNIT_EXPECT_EQ(test, verdict.scsid, 2);
+
+ /* A frame without ports leaves only the address descriptor */
+ KUNIT_ASSERT_TRUE(test,
+ cfg80211_flow_parse(TEST_SKB(test, udp_fragment),
+ &info));
+ cfg80211_scs_evaluate(list, 3, &info, &verdict);
+ KUNIT_EXPECT_TRUE(test, verdict.match);
+ KUNIT_EXPECT_EQ(test, verdict.scsid, 1);
+ KUNIT_EXPECT_EQ(test, verdict.up, 3);
+}
+
+/* A key learned from an uplink frame must describe the downlink direction */
+static void flow_key_mirror(struct kunit *test)
+{
+ static const u8 mask_elem[] = {
+ WLAN_EID_EXTENSION, 19,
+ WLAN_EID_EXT_TCLAS_MASK,
+ 4, 0x1e,
+ 4,
+ 0, 0, 0, 0,
+ 0, 0, 0, 0,
+ 0, 0, 0, 0,
+ 0, 0, 0,
+ };
+ struct cfg80211_flow_key as_is, mirrored;
+ struct cfg80211_flow_info info;
+ u32 fields;
+
+ KUNIT_ASSERT_EQ(test, 0,
+ cfg80211_parse_tclas_mask(mask_elem, sizeof(mask_elem),
+ &fields));
+ KUNIT_EXPECT_EQ(test, fields,
+ FLOW_F(IP_VERSION) | FLOW_F(IP_SRC) | FLOW_F(IP_DST) |
+ FLOW_F(SRC_PORT) | FLOW_F(DST_PORT));
+
+ KUNIT_ASSERT_TRUE(test,
+ cfg80211_flow_parse(TEST_SKB(test, udp_frame),
+ &info));
+
+ KUNIT_ASSERT_TRUE(test, cfg80211_flow_key_build(&info, fields,
+ CFG80211_FLOW_AS_IS,
+ &as_is));
+ KUNIT_ASSERT_TRUE(test, cfg80211_flow_key_build(&info, fields,
+ CFG80211_FLOW_MIRRORED,
+ &mirrored));
+
+ KUNIT_EXPECT_MEMEQ(test, &as_is.src, &mirrored.dst, sizeof(as_is.src));
+ KUNIT_EXPECT_MEMEQ(test, &as_is.dst, &mirrored.src, sizeof(as_is.dst));
+ KUNIT_EXPECT_EQ(test, be16_to_cpu(as_is.src_port),
+ be16_to_cpu(mirrored.dst_port));
+ KUNIT_EXPECT_EQ(test, be16_to_cpu(as_is.dst_port),
+ be16_to_cpu(mirrored.src_port));
+
+ /* No unselected field may reach the key */
+ KUNIT_EXPECT_MEMEQ(test, as_is.sa, "\x00\x00\x00\x00\x00\x00",
+ ETH_ALEN);
+ KUNIT_EXPECT_EQ(test, be16_to_cpu(as_is.eth_type), 0);
+ KUNIT_EXPECT_EQ(test, as_is.proto, 0);
+ KUNIT_EXPECT_EQ(test, as_is.ip_version, 4);
+ KUNIT_EXPECT_EQ(test, mirrored.ip_version, 4);
+}
+
+/* The Ethernet addresses mirror as a pair */
+static void flow_key_mirror_eth(struct kunit *test)
+{
+ static const u8 mask_elem[] = {
+ WLAN_EID_EXTENSION, 17,
+ WLAN_EID_EXT_TCLAS_MASK,
+ 0, 0x01, /* classifier type 0, source address */
+ 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0,
+ 0, 0,
+ };
+ struct cfg80211_flow_key key;
+ struct cfg80211_flow_info info;
+ u32 fields;
+
+ KUNIT_ASSERT_EQ(test, 0,
+ cfg80211_parse_tclas_mask(mask_elem, sizeof(mask_elem),
+ &fields));
+ KUNIT_EXPECT_EQ(test, fields, FLOW_F(ETH_SA));
+
+ KUNIT_ASSERT_TRUE(test,
+ cfg80211_flow_parse(TEST_SKB(test, udp_frame),
+ &info));
+
+ KUNIT_ASSERT_TRUE(test, cfg80211_flow_key_build(&info, fields,
+ CFG80211_FLOW_MIRRORED,
+ &key));
+
+ KUNIT_EXPECT_MEMEQ(test, key.sa, test_da, ETH_ALEN);
+ KUNIT_EXPECT_MEMEQ(test, key.da, "\x00\x00\x00\x00\x00\x00", ETH_ALEN);
+}
+
+/* Each unsupported classifier is refused with -EOPNOTSUPP, not -EINVAL */
+static void tclas_refused(struct kunit *test)
+{
+ static const u8 mac_header[] = {
+ WLAN_EID_TCLAS, 5,
+ 255,
+ 6, 0x00, 0x00, 0x00, /* type 6, 3 octet mask */
+ };
+ static const u8 up_not_255[] = {
+ WLAN_EID_TCLAS, 17,
+ 0, /* a UP that takes part in the match */
+ 0, 0x01,
+ 0x02, 0x00, 0x00, 0x00, 0x00, 0x01,
+ 0x02, 0x00, 0x00, 0x00, 0x00, 0x02,
+ 0x08, 0x00,
+ };
+ static const u8 filter_in_mask[] = {
+ WLAN_EID_EXTENSION, 9,
+ WLAN_EID_EXT_TCLAS_MASK,
+ 3, 0,
+ 0, 0,
+ 0x00, 0x00,
+ 0xff, 0xff,
+ };
+ static const u8 legacy_ip[] = {
+ WLAN_EID_TCLAS, 19,
+ 255,
+ 1, 0x06,
+ 4,
+ 192, 168, 1, 1,
+ 192, 168, 1, 2,
+ 0, 0, 0, 0,
+ 0, 0, 0,
+ };
+ static const u8 legacy_vlan[] = {
+ WLAN_EID_TCLAS, 5,
+ 255,
+ 2, 0x01,
+ 0x00, 0x64,
+ };
+ enum cfg80211_tclas_processing processing;
+ struct cfg80211_tclas tclas;
+ u32 fields;
+
+ KUNIT_EXPECT_EQ(test, -EOPNOTSUPP,
+ cfg80211_parse_tclas(mac_header, sizeof(mac_header),
+ &tclas, 1, &processing));
+ KUNIT_EXPECT_EQ(test, -EOPNOTSUPP,
+ cfg80211_parse_tclas(up_not_255, sizeof(up_not_255),
+ &tclas, 1, &processing));
+ KUNIT_EXPECT_EQ(test, -EOPNOTSUPP,
+ cfg80211_parse_tclas(legacy_ip, sizeof(legacy_ip),
+ &tclas, 1, &processing));
+ KUNIT_EXPECT_EQ(test, -EOPNOTSUPP,
+ cfg80211_parse_tclas(legacy_vlan, sizeof(legacy_vlan),
+ &tclas, 1, &processing));
+ KUNIT_EXPECT_EQ(test, -EOPNOTSUPP,
+ cfg80211_parse_tclas_mask(filter_in_mask,
+ sizeof(filter_in_mask),
+ &fields));
+}
+
+/* The presence bitmap decides where every optional field starts */
+static void qos_characteristics(struct kunit *test)
+{
+ /* Downlink, TID 5, UP 5, and a Service Start Time with no LinkID */
+ static const u8 no_link_id[] = {
+ 0xff, 0x17, 0x71, /* element header, 113 */
+ 0x55, 0x05, 0x00, 0x00, /* control info, presence bit 1 */
+ 0xe8, 0x03, 0x00, 0x00, /* minimum service interval */
+ 0xd0, 0x07, 0x00, 0x00, /* maximum service interval */
+ 0x88, 0x13, 0x00, /* minimum data rate */
+ 0x20, 0x4e, 0x00, /* delay bound */
+ 0x78, 0x56, 0x34, 0x12, /* service start time */
+ };
+ static const u8 with_link_id[] = {
+ 0xff, 0x18, 0x71,
+ 0x55, 0x0d, 0x00, 0x00, /* presence bits 1 and 2 */
+ 0xe8, 0x03, 0x00, 0x00,
+ 0xd0, 0x07, 0x00, 0x00,
+ 0x88, 0x13, 0x00,
+ 0x20, 0x4e, 0x00,
+ 0x78, 0x56, 0x34, 0x12,
+ 0x03, /* service start time LinkID */
+ };
+ static const u8 orphan_link_id[] = {
+ 0xff, 0x14, 0x71,
+ 0x55, 0x09, 0x00, 0x00, /* presence bit 2 alone */
+ 0xe8, 0x03, 0x00, 0x00,
+ 0xd0, 0x07, 0x00, 0x00,
+ 0x88, 0x13, 0x00,
+ 0x20, 0x4e, 0x00,
+ 0x03,
+ };
+ static const u8 bad_ext_id[] = {
+ 0xff, 0x17, 0x70,
+ 0x55, 0x05, 0x00, 0x00,
+ 0xe8, 0x03, 0x00, 0x00,
+ 0xd0, 0x07, 0x00, 0x00,
+ 0x88, 0x13, 0x00,
+ 0x20, 0x4e, 0x00,
+ 0x78, 0x56, 0x34, 0x12,
+ };
+ static const u8 bad_length[] = {
+ 0xff, 0x18, 0x71,
+ 0x55, 0x05, 0x00, 0x00,
+ 0xe8, 0x03, 0x00, 0x00,
+ 0xd0, 0x07, 0x00, 0x00,
+ 0x88, 0x13, 0x00,
+ 0x20, 0x4e, 0x00,
+ 0x78, 0x56, 0x34, 0x12,
+ };
+ static const u8 short_elem[] = {
+ 0xff, 0x13, 0x71,
+ 0x55, 0x05, 0x00, 0x00,
+ 0xe8, 0x03, 0x00, 0x00,
+ 0xd0, 0x07, 0x00, 0x00,
+ 0x88, 0x13, 0x00,
+ 0x20, 0x4e, 0x00,
+ };
+
+ KUNIT_EXPECT_TRUE(test, QC_OK(no_link_id));
+ KUNIT_EXPECT_EQ(test, ieee80211_qos_char_direction(QC(no_link_id)),
+ IEEE80211_QOS_CHAR_DIR_DOWNLINK);
+ KUNIT_EXPECT_EQ(test, ieee80211_qos_char_presence(QC(no_link_id)),
+ IEEE80211_QOS_CHAR_PRES_SERVICE_START_TIME);
+
+ KUNIT_EXPECT_TRUE(test, QC_OK(with_link_id));
+ KUNIT_EXPECT_EQ(test, ieee80211_qos_char_presence(QC(with_link_id)),
+ IEEE80211_QOS_CHAR_PRES_SERVICE_START_TIME |
+ IEEE80211_QOS_CHAR_PRES_SERVICE_START_LINK_ID);
+
+ KUNIT_EXPECT_FALSE(test, QC_OK(orphan_link_id));
+ KUNIT_EXPECT_FALSE(test, QC_OK(short_elem));
+ KUNIT_EXPECT_FALSE(test, QC_OK(bad_ext_id));
+ KUNIT_EXPECT_FALSE(test, QC_OK(bad_length));
+}
+
+/* An uplink descriptor carries no classifier and no user priority */
+static void scs_traffic_description(struct kunit *test)
+{
+ /* Uplink, and a Service Start Time with no LinkID */
+ static const u8 uplink[] = {
+ 0xff, 0x17, 0x71,
+ 0x54, 0x05, 0x00, 0x00,
+ 0xe8, 0x03, 0x00, 0x00,
+ 0xd0, 0x07, 0x00, 0x00,
+ 0x88, 0x13, 0x00,
+ 0x20, 0x4e, 0x00,
+ 0x78, 0x56, 0x34, 0x12,
+ };
+ struct cfg80211_scs_desc desc = {
+ .req_type = NL80211_SCS_REQ_ADD,
+ .tclas_processing = CFG80211_TCLAS_PROCESSING_ABSENT,
+ .qos_char = QC(uplink),
+ .qos_char_len = sizeof(uplink),
+ .id = 1,
+ };
+
+ KUNIT_ASSERT_TRUE(test, QC_OK(uplink));
+ KUNIT_ASSERT_EQ(test, ieee80211_qos_char_direction(QC(uplink)),
+ IEEE80211_QOS_CHAR_DIR_UPLINK);
+
+ KUNIT_EXPECT_TRUE(test, cfg80211_scs_desc_valid(&desc));
+
+ /* Such a descriptor carries no TCLAS Processing element either */
+ desc.tclas_processing = CFG80211_TCLAS_PROCESSING_DEFAULT;
+ KUNIT_EXPECT_FALSE(test, cfg80211_scs_desc_valid(&desc));
+}
+
+/* A filter classifier matches on the payload octets of the named protocol */
+static void scs_filter(struct kunit *test)
+{
+ static const u8 udp_sport[] = {
+ WLAN_EID_TCLAS, 8,
+ 255,
+ 10, 0, 17, /* type, instance, protocol */
+ 0x13, 0x88, /* value: source port 5000 */
+ 0xff, 0xff, /* mask */
+ };
+ static const u8 wrong_sport[] = {
+ WLAN_EID_TCLAS, 8,
+ 255,
+ 10, 0, 17,
+ 0x00, 0x35,
+ 0xff, 0xff,
+ };
+ struct cfg80211_scs_desc *list[1];
+ struct cfg80211_scs_verdict verdict;
+ struct cfg80211_flow_info info;
+
+ KUNIT_ASSERT_TRUE(test,
+ cfg80211_flow_parse(TEST_SKB(test, udp_frame),
+ &info));
+
+ list[0] = test_scs_desc(test, 1, 5, udp_sport, sizeof(udp_sport));
+ KUNIT_EXPECT_EQ(test, list[0]->tclas[0].type, 10);
+ KUNIT_EXPECT_EQ(test, list[0]->tclas[0].filter.len, 2);
+
+ cfg80211_scs_evaluate(list, 1, &info, &verdict);
+ KUNIT_EXPECT_TRUE(test, verdict.match);
+ KUNIT_EXPECT_EQ(test, verdict.up, 5);
+
+ list[0] = test_scs_desc(test, 2, 5, wrong_sport, sizeof(wrong_sport));
+ cfg80211_scs_evaluate(list, 1, &info, &verdict);
+ KUNIT_EXPECT_FALSE(test, verdict.match);
+}
+
+static struct kunit_case scs_cases[] = {
+ KUNIT_CASE(flow_parse_fragment),
+ KUNIT_CASE(scs_granularity),
+ KUNIT_CASE(scs_filter),
+ KUNIT_CASE(flow_key_mirror),
+ KUNIT_CASE(flow_key_mirror_eth),
+ KUNIT_CASE(tclas_refused),
+ KUNIT_CASE(qos_characteristics),
+ KUNIT_CASE(scs_traffic_description),
+ {}
+};
+
+static struct kunit_suite scs = {
+ .name = "cfg80211-scs",
+ .test_cases = scs_cases,
+};
+
+kunit_test_suite(scs);
diff --git a/net/wireless/trace.h b/net/wireless/trace.h
index 8c2a91b85c396..81c0899cb212c 100644
--- a/net/wireless/trace.h
+++ b/net/wireless/trace.h
@@ -2636,6 +2636,55 @@ TRACE_EVENT(rdev_set_ap_chanwidth,
__entry->link_id)
);
+TRACE_EVENT(rdev_set_scs,
+ TP_PROTO(struct wiphy *wiphy, struct net_device *netdev,
+ const u8 *peer, u8 n_desc),
+ TP_ARGS(wiphy, netdev, peer, n_desc),
+ TP_STRUCT__entry(
+ WIPHY_ENTRY
+ NETDEV_ENTRY
+ MAC_ENTRY(peer)
+ __field(u8, n_desc)
+ ),
+ TP_fast_assign(
+ WIPHY_ASSIGN;
+ NETDEV_ASSIGN;
+ MAC_ASSIGN(peer, peer);
+ __entry->n_desc = n_desc;
+ ),
+ TP_printk(WIPHY_PR_FMT ", " NETDEV_PR_FMT ", %pM, %d descriptors",
+ WIPHY_PR_ARG, NETDEV_PR_ARG, __entry->peer, __entry->n_desc)
+);
+
+TRACE_EVENT(rdev_set_mscs,
+ TP_PROTO(struct wiphy *wiphy, struct net_device *netdev,
+ const u8 *peer, const struct cfg80211_mscs_desc *desc),
+ TP_ARGS(wiphy, netdev, peer, desc),
+ TP_STRUCT__entry(
+ WIPHY_ENTRY
+ NETDEV_ENTRY
+ MAC_ENTRY(peer)
+ __field(u32, fields)
+ __field(u32, stream_timeout)
+ __field(u8, up_bitmap)
+ __field(u8, up_limit)
+ ),
+ TP_fast_assign(
+ WIPHY_ASSIGN;
+ NETDEV_ASSIGN;
+ MAC_ASSIGN(peer, peer);
+ __entry->fields = desc ? desc->fields : 0;
+ __entry->stream_timeout = desc ? desc->stream_timeout : 0;
+ __entry->up_bitmap = desc ? desc->up_bitmap : 0;
+ __entry->up_limit = desc ? desc->up_limit : 0;
+ ),
+ TP_printk(WIPHY_PR_FMT ", " NETDEV_PR_FMT
+ ", %pM, fields %x, UP bitmap %x, UP limit %d, timeout %d",
+ WIPHY_PR_ARG, NETDEV_PR_ARG, __entry->peer, __entry->fields,
+ __entry->up_bitmap, __entry->up_limit,
+ __entry->stream_timeout)
+);
+
TRACE_EVENT(rdev_add_tx_ts,
TP_PROTO(struct wiphy *wiphy, struct net_device *netdev,
u8 tsid, const u8 *peer, u8 user_prio, u16 admitted_time),
|
| 3/2 | 2026/09/24 13:41 | llm | patch-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 60737 output: 40 thoughts: 1335 Thoughts: **Patch Series Evaluation for Syzkaller Fuzzing** Okay, so I need to assess this patch series for fuzzing with syzkaller. First, I need to understand what's changed and whether the changes are reachable in a fuzzing context. Alright, the diff touches a significant chunk of the wireless networking stack. Specifically, I see changes in `drivers/net/wireless/mediatek/mt76/...`, `drivers/net/wireless/virtual/mac80211_hwsim_...`, and a ton of core includes and source files in `net/mac80211` and `net/wireless`. I notice it's touching the standard virtual driver `mac80211_hwsim`, which is excellent for syzkaller, this is a standard configuration. I also notice a new file `scs.c` in both `net/mac80211` and `net/wireless`. It looks like new netlink commands (`NL80211_CMD_SET_SCS`, `NL80211_CMD_SET_MSCS`) are introduced through `nl80211` and these are handled by associated functions. Then those are implemented by mac80211, and further flow classification and learning functions in packet reception/transmission paths are introduced. The virtual driver `mac80211_hwsim` advertises support and implements its own `set_scs` function. There are also debugfs entries added which is a bonus for debugging. And a bunch of code parsing TCLAS, TCLAS mask, and flows in `net/wireless/scs.c`. Given that these changes involve core wireless subsystems and the virtual `mac80211_hwsim` driver, this is *definitely* reachable in a standard syzkaller setup, especially those using QEMU. This patch series is a prime candidate for fuzzing. `WorthFuzzing = true`. Now, the focus. I need to identify specific, non-ubiquitous functions that would be good targets for targeted fuzzing. I need to get some more precise focus symbols. Let's see... the added or modified functions are: `nl80211_set_scs`, `nl80211_set_mscs`, `nl80211_parse_scs_desc`, `ieee80211_set_scs`, `ieee80211_set_mscs`, `ieee80211_flow_classify`, `ieee80211_flow_learn`, `ieee80211_flow_insert`, `ieee80211_flow_gc_work`, `cfg80211_flow_parse`, `cfg80211_flow_key_build`, `cfg80211_scs_evaluate`, `cfg80211_parse_tclas`, `cfg80211_parse_tclas_mask`, and `mac80211_hwsim_sta_set_scs`. Now, I'll need to check if these are static/exported or visible kernel symbols. I'll use `nm`, `kallsyms`, `objdump`, or a code search tool like `grepper` to determine their visibility and gather more context on them. |
| 4/2 | 2026/09/24 13:42 | tool | grepper |
0mArgs:{
"Expression": "ieee80211_set_scs",
"PathPrefix": "net/mac80211"
}Results: {
"Output": "net/mac80211/cfg.c=5990=const struct cfg80211_ops mac80211_config_ops = {\n--\nnet/mac80211/cfg.c-6075-\t.set_qos_map = ieee80211_set_qos_map,\nnet/mac80211/cfg.c:6076:\t.set_scs = ieee80211_set_scs,\nnet/mac80211/cfg.c-6077-\t.set_mscs = ieee80211_set_mscs,\n--\nnet/mac80211/ieee80211_i.h=2219=void ieee80211_recalc_offload(struct ieee80211_local *local);\n--\nnet/mac80211/ieee80211_i.h-2221-/* SCS and MSCS */\nnet/mac80211/ieee80211_i.h:2222:int ieee80211_set_scs(struct wiphy *wiphy, struct net_device *dev,\nnet/mac80211/ieee80211_i.h-2223-\t\t const u8 *peer, struct cfg80211_scs_desc * const *desc,\n--\nnet/mac80211/scs.c=335=void ieee80211_flow_learn(struct ieee80211_rx_data *rx)\n--\nnet/mac80211/scs.c-379-\nnet/mac80211/scs.c:380:int ieee80211_set_scs(struct wiphy *wiphy, struct net_device *dev,\nnet/mac80211/scs.c-381-\t\t const u8 *peer, struct cfg80211_scs_desc * const *desc,\n"
} |
| 5/2 | 2026/09/24 13:42 | llm | patch-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 63393 output: 37 thoughts: 62 Thoughts: Okay, here's my thought process on how I'd approach this: **Investigating Kconfig and Driver Configurations** Alright, I need to get a handle on the relevant configuration options and drivers, so my immediate thought is to start with the Kconfig files. I'm going to dig into `net/mac80211/Kconfig`, `net/wireless/Kconfig`, and then maybe peek at `drivers/net/wireless/virtual/mac80211_hwsim.c` (or its associated Kconfig, if it has one). These are the typical locations for networking and wireless configuration settings. I'm thinking, "Let's use a grep search to query within Kconfig files". That should give me a solid overview of what's available and how things are defined. It's the most efficient way to see how the features are controlled. |
| 6/2 | 2026/09/24 13:42 | tool | grepper |
0mArgs:{
"Expression": "MAC80211_HWSIM",
"PathPrefix": "drivers/net/wireless/virtual"
}Results: {
"Output": "drivers/net/wireless/virtual/Kconfig:1:config MAC80211_HWSIM\ndrivers/net/wireless/virtual/Kconfig-2-\ttristate \"Simulated radio testing tool for mac80211\"\n--\ndrivers/net/wireless/virtual/Makefile:1:obj-$(CONFIG_MAC80211_HWSIM)\t+= mac80211_hwsim.o\ndrivers/net/wireless/virtual/Makefile-2-mac80211_hwsim-objs\t\t+= mac80211_hwsim_main.o\n--\ndrivers/net/wireless/virtual/mac80211_hwsim.h-8-\ndrivers/net/wireless/virtual/mac80211_hwsim.h:9:#ifndef __MAC80211_HWSIM_H\ndrivers/net/wireless/virtual/mac80211_hwsim.h:10:#define __MAC80211_HWSIM_H\ndrivers/net/wireless/virtual/mac80211_hwsim.h-11-\n--\ndrivers/net/wireless/virtual/mac80211_hwsim.h=220=struct hwsim_tx_rate {\n--\ndrivers/net/wireless/virtual/mac80211_hwsim.h-233- *\ndrivers/net/wireless/virtual/mac80211_hwsim.h:234: * @MAC80211_HWSIM_TX_RC_USE_RTS_CTS: Use RTS/CTS exchange for this rate.\ndrivers/net/wireless/virtual/mac80211_hwsim.h:235: * @MAC80211_HWSIM_TX_RC_USE_CTS_PROTECT: CTS-to-self protection is required.\ndrivers/net/wireless/virtual/mac80211_hwsim.h-236- *\tThis is set if the current BSS requires ERP protection.\ndrivers/net/wireless/virtual/mac80211_hwsim.h:237: * @MAC80211_HWSIM_TX_RC_USE_SHORT_PREAMBLE: Use short preamble.\ndrivers/net/wireless/virtual/mac80211_hwsim.h:238: * @MAC80211_HWSIM_TX_RC_MCS: HT rate.\ndrivers/net/wireless/virtual/mac80211_hwsim.h:239: * @MAC80211_HWSIM_TX_RC_VHT_MCS: VHT MCS rate, in this case the idx field is\ndrivers/net/wireless/virtual/mac80211_hwsim.h-240- *\tsplit into a higher 4 bits (Nss) and lower 4 bits (MCS number)\ndrivers/net/wireless/virtual/mac80211_hwsim.h:241: * @MAC80211_HWSIM_TX_RC_GREEN_FIELD: Indicates whether this rate should be used\ndrivers/net/wireless/virtual/mac80211_hwsim.h-242- *\tin Greenfield mode.\ndrivers/net/wireless/virtual/mac80211_hwsim.h:243: * @MAC80211_HWSIM_TX_RC_40_MHZ_WIDTH: Indicates if the Channel Width should be\ndrivers/net/wireless/virtual/mac80211_hwsim.h-244- *\t40 MHz.\ndrivers/net/wireless/virtual/mac80211_hwsim.h:245: * @MAC80211_HWSIM_TX_RC_80_MHZ_WIDTH: Indicates 80 MHz transmission\ndrivers/net/wireless/virtual/mac80211_hwsim.h:246: * @MAC80211_HWSIM_TX_RC_160_MHZ_WIDTH: Indicates 160 MHz transmission\ndrivers/net/wireless/virtual/mac80211_hwsim.h-247- *\t(80+80 isn't supported yet)\ndrivers/net/wireless/virtual/mac80211_hwsim.h:248: * @MAC80211_HWSIM_TX_RC_DUP_DATA: The frame should be transmitted on both of\ndrivers/net/wireless/virtual/mac80211_hwsim.h-249- *\tthe adjacent 20 MHz channels, if the current channel type is\ndrivers/net/wireless/virtual/mac80211_hwsim.h-250- *\tNL80211_CHAN_HT40MINUS or NL80211_CHAN_HT40PLUS.\ndrivers/net/wireless/virtual/mac80211_hwsim.h:251: * @MAC80211_HWSIM_TX_RC_SHORT_GI: Short Guard interval should be used for this\ndrivers/net/wireless/virtual/mac80211_hwsim.h-252- *\trate.\n--\ndrivers/net/wireless/virtual/mac80211_hwsim.h=254=enum hwsim_tx_rate_flags {\ndrivers/net/wireless/virtual/mac80211_hwsim.h:255:\tMAC80211_HWSIM_TX_RC_USE_RTS_CTS\t\t= BIT(0),\ndrivers/net/wireless/virtual/mac80211_hwsim.h:256:\tMAC80211_HWSIM_TX_RC_USE_CTS_PROTECT\t\t= BIT(1),\ndrivers/net/wireless/virtual/mac80211_hwsim.h:257:\tMAC80211_HWSIM_TX_RC_USE_SHORT_PREAMBLE\t= BIT(2),\ndrivers/net/wireless/virtual/mac80211_hwsim.h-258-\ndrivers/net/wireless/virtual/mac80211_hwsim.h-259-\t/* rate index is an HT/VHT MCS instead of an index */\ndrivers/net/wireless/virtual/mac80211_hwsim.h:260:\tMAC80211_HWSIM_TX_RC_MCS\t\t\t= BIT(3),\ndrivers/net/wireless/virtual/mac80211_hwsim.h:261:\tMAC80211_HWSIM_TX_RC_GREEN_FIELD\t\t= BIT(4),\ndrivers/net/wireless/virtual/mac80211_hwsim.h:262:\tMAC80211_HWSIM_TX_RC_40_MHZ_WIDTH\t\t= BIT(5),\ndrivers/net/wireless/virtual/mac80211_hwsim.h:263:\tMAC80211_HWSIM_TX_RC_DUP_DATA\t\t= BIT(6),\ndrivers/net/wireless/virtual/mac80211_hwsim.h:264:\tMAC80211_HWSIM_TX_RC_SHORT_GI\t\t= BIT(7),\ndrivers/net/wireless/virtual/mac80211_hwsim.h:265:\tMAC80211_HWSIM_TX_RC_VHT_MCS\t\t\t= BIT(8),\ndrivers/net/wireless/virtual/mac80211_hwsim.h:266:\tMAC80211_HWSIM_TX_RC_80_MHZ_WIDTH\t\t= BIT(9),\ndrivers/net/wireless/virtual/mac80211_hwsim.h:267:\tMAC80211_HWSIM_TX_RC_160_MHZ_WIDTH\t\t= BIT(10),\ndrivers/net/wireless/virtual/mac80211_hwsim.h-268-};\n--\ndrivers/net/wireless/virtual/mac80211_hwsim.h=331=enum hwsim_rate_info_attributes {\n--\ndrivers/net/wireless/virtual/mac80211_hwsim.h-350-\ndrivers/net/wireless/virtual/mac80211_hwsim.h:351:#endif /* __MAC80211_HWSIM_H */\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_i.h-9-\ndrivers/net/wireless/virtual/mac80211_hwsim_i.h:10:#ifndef __MAC80211_HWSIM_I_H\ndrivers/net/wireless/virtual/mac80211_hwsim_i.h:11:#define __MAC80211_HWSIM_I_H\ndrivers/net/wireless/virtual/mac80211_hwsim_i.h-12-\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_i.h=168=void ieee80211_hwsim_wake_tx_queue(struct ieee80211_hw *hw,\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_i.h-170-\ndrivers/net/wireless/virtual/mac80211_hwsim_i.h:171:#endif /* __MAC80211_HWSIM_I_H */\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c=712=static struct mac80211_hwsim_data *get_hwsim_data_ref_from_addr(const u8 *addr)\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c-716-\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c:717:/* MAC80211_HWSIM netlink family */\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c-718-static struct genl_family hwsim_genl_family;\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c=724=static const struct genl_multicast_group hwsim_mcgrps[] = {\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c-727-\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c:728:/* MAC80211_HWSIM netlink policy */\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c-729-\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c=865=static const struct nla_policy hwsim_genl_policy[HWSIM_ATTR_MAX + 1] = {\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c-902-\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c:903:/* MAC80211_HWSIM virtio queues */\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c-904-static struct virtqueue *hwsim_vqs[HWSIM_NUM_VQS];\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c=1575=static inline u16 trans_tx_rate_flags_ieee2hwsim(struct ieee80211_tx_rate *rate)\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c-1579-\tif (rate-\u003eflags \u0026 IEEE80211_TX_RC_USE_RTS_CTS)\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c:1580:\t\tresult |= MAC80211_HWSIM_TX_RC_USE_RTS_CTS;\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c-1581-\tif (rate-\u003eflags \u0026 IEEE80211_TX_RC_USE_CTS_PROTECT)\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c:1582:\t\tresult |= MAC80211_HWSIM_TX_RC_USE_CTS_PROTECT;\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c-1583-\tif (rate-\u003eflags \u0026 IEEE80211_TX_RC_USE_SHORT_PREAMBLE)\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c:1584:\t\tresult |= MAC80211_HWSIM_TX_RC_USE_SHORT_PREAMBLE;\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c-1585-\tif (rate-\u003eflags \u0026 IEEE80211_TX_RC_MCS)\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c:1586:\t\tresult |= MAC80211_HWSIM_TX_RC_MCS;\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c-1587-\tif (rate-\u003eflags \u0026 IEEE80211_TX_RC_GREEN_FIELD)\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c:1588:\t\tresult |= MAC80211_HWSIM_TX_RC_GREEN_FIELD;\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c-1589-\tif (rate-\u003eflags \u0026 IEEE80211_TX_RC_40_MHZ_WIDTH)\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c:1590:\t\tresult |= MAC80211_HWSIM_TX_RC_40_MHZ_WIDTH;\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c-1591-\tif (rate-\u003eflags \u0026 IEEE80211_TX_RC_DUP_DATA)\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c:1592:\t\tresult |= MAC80211_HWSIM_TX_RC_DUP_DATA;\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c-1593-\tif (rate-\u003eflags \u0026 IEEE80211_TX_RC_SHORT_GI)\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c:1594:\t\tresult |= MAC80211_HWSIM_TX_RC_SHORT_GI;\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c-1595-\tif (rate-\u003eflags \u0026 IEEE80211_TX_RC_VHT_MCS)\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c:1596:\t\tresult |= MAC80211_HWSIM_TX_RC_VHT_MCS;\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c-1597-\tif (rate-\u003eflags \u0026 IEEE80211_TX_RC_80_MHZ_WIDTH)\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c:1598:\t\tresult |= MAC80211_HWSIM_TX_RC_80_MHZ_WIDTH;\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c-1599-\tif (rate-\u003eflags \u0026 IEEE80211_TX_RC_160_MHZ_WIDTH)\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c:1600:\t\tresult |= MAC80211_HWSIM_TX_RC_160_MHZ_WIDTH;\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c-1601-\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c=3489=static const char mac80211_hwsim_gstrings_stats[][ETH_GSTRING_LEN] = {\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c-3499-\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c:3500:#define MAC80211_HWSIM_SSTATS_LEN ARRAY_SIZE(mac80211_hwsim_gstrings_stats)\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c-3501-\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c=3511=static int mac80211_hwsim_get_et_sset_count(struct ieee80211_hw *hw,\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c-3514-\tif (sset == ETH_SS_STATS)\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c:3515:\t\treturn MAC80211_HWSIM_SSTATS_LEN;\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c-3516-\treturn 0;\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c=3519=static void mac80211_hwsim_get_et_stats(struct ieee80211_hw *hw,\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c-3534-\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c:3535:\tWARN_ON(i != MAC80211_HWSIM_SSTATS_LEN);\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c-3536-}\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c=5672=static const u8 iftypes_ext_capa_ap[] = {\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c-5680-\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c:5681:#define MAC80211_HWSIM_MLD_CAPA_OPS\t\t\t\t\\\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c-5682-\tFIELD_PREP_CONST(IEEE80211_MLD_CAP_OP_TID_TO_LINK_MAP_NEG_SUPP, \\\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c=5687=static const struct wiphy_iftype_ext_capab mac80211_hwsim_iftypes_ext_capa[] = {\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c-5694-\t\t\t\t IEEE80211_EML_CAP_EMLMR_SUPPORT,\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c:5695:\t\t.mld_capa_and_ops = MAC80211_HWSIM_MLD_CAPA_OPS,\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c-5696-\t},\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c=7229=static struct genl_family hwsim_genl_family __ro_after_init = {\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c:7230:\t.name = \"MAC80211_HWSIM\",\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c-7231-\t.version = 1,\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c=7549=static void hwsim_virtio_remove(struct virtio_device *vdev)\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c-7557-\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c:7558:/* MAC80211_HWSIM virtio device id table */\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c-7559-static const struct virtio_device_id id_table[] = {\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c:7560:\t{ VIRTIO_ID_MAC80211_HWSIM, VIRTIO_DEV_ANY_ID },\ndrivers/net/wireless/virtual/mac80211_hwsim_main.c-7561-\t{ 0 }\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c=78=static u64 hwsim_nan_get_master_rank(struct mac80211_hwsim_data *data)\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c-82-\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c:83:\tif (data-\u003enan.phase == MAC80211_HWSIM_NAN_PHASE_UP) {\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c-84-\t\tmaster_pref = data-\u003enan.master_pref;\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c=216=void mac80211_hwsim_nan_rx(struct ieee80211_hw *hw,\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c-273-\tif ((slot != SLOT_24GHZ_DW \u0026\u0026\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c:274:\t data-\u003enan.phase != MAC80211_HWSIM_NAN_PHASE_SCAN) ||\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c-275-\t rx_status.freq != 2437)\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c-436-\t\t (cg == own_cg \u0026\u0026 frame_amr \u003e curr_amr) ||\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c:437:\t\t data-\u003enan.phase == MAC80211_HWSIM_NAN_PHASE_SCAN) {\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c-438-\t\t\t/* Avoid a state transition */\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c-468-\t\t\t */\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c:469:\t\t\tdata-\u003enan.phase = MAC80211_HWSIM_NAN_PHASE_UP;\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c-470-\t\t\tdata-\u003enan.random_factor_valid_dwst = 0;\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c=612=mac80211_hwsim_nan_exec_state_transitions(struct mac80211_hwsim_data *data)\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c-625-\tif (data-\u003enan.master_transition_score \u003c 3)\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c:626:\t\tdata-\u003enan.role = MAC80211_HWSIM_NAN_ROLE_MASTER;\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c:627:\telse if (data-\u003enan.role == MAC80211_HWSIM_NAN_ROLE_MASTER \u0026\u0026\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c-628-\t\t data-\u003enan.master_transition_score \u003e= 3)\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c:629:\t\tdata-\u003enan.role = MAC80211_HWSIM_NAN_ROLE_SYNC;\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c:630:\telse if (data-\u003enan.role == MAC80211_HWSIM_NAN_ROLE_SYNC \u0026\u0026\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c-631-\t\t data-\u003enan.sync_transition_score \u003e= 3)\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c:632:\t\tdata-\u003enan.role = MAC80211_HWSIM_NAN_ROLE_NON_SYNC;\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c:633:\telse if (data-\u003enan.role == MAC80211_HWSIM_NAN_ROLE_NON_SYNC \u0026\u0026\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c-634-\t\t data-\u003enan.sync_transition_score \u003c 3)\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c:635:\t\tdata-\u003enan.role = MAC80211_HWSIM_NAN_ROLE_SYNC;\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c-636-\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c-641-\t */\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c:642:\tif (data-\u003enan.role == MAC80211_HWSIM_NAN_ROLE_MASTER \u0026\u0026\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c-643-\t !hrtimer_active(\u0026data-\u003enan.discovery_beacon_timer))\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c-653-\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c:654:\t\tif (data-\u003enan.phase == MAC80211_HWSIM_NAN_PHASE_SCAN) {\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c:655:\t\t\tdata-\u003enan.phase = MAC80211_HWSIM_NAN_PHASE_WARMUP;\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c-656-\t\t\tdata-\u003enan.random_factor_valid_dwst = NAN_WARMUP_DWST;\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c-659-\t\t} else {\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c:660:\t\t\tdata-\u003enan.phase = MAC80211_HWSIM_NAN_PHASE_UP;\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c-661-\t\t\tdata-\u003enan.random_factor_valid_dwst =\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c=695=mac80211_hwsim_nan_tx_beacon(struct mac80211_hwsim_data *data,\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c-750-\tnan_attr.length = cpu_to_le16(sizeof(master_indication));\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c:751:\tif (data-\u003enan.phase == MAC80211_HWSIM_NAN_PHASE_UP) {\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c-752-\t\tmaster_indication.master_pref = data-\u003enan.master_pref;\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c=774=mac80211_hwsim_nan_slot_timer(struct hrtimer *timer)\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c-807-\t\ttx_sync_beacon =\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c:808:\t\t\tdata-\u003enan.phase != MAC80211_HWSIM_NAN_PHASE_SCAN \u0026\u0026\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c:809:\t\t\tdata-\u003enan.role != MAC80211_HWSIM_NAN_ROLE_NON_SYNC;\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c-810-\t}\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c=867=mac80211_hwsim_nan_discovery_beacon_timer(struct hrtimer *timer)\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c-879-\tscoped_guard(spinlock, \u0026data-\u003enan.state_lock) {\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c:880:\t\tif (data-\u003enan.phase == MAC80211_HWSIM_NAN_PHASE_SCAN ||\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c:881:\t\t data-\u003enan.role != MAC80211_HWSIM_NAN_ROLE_MASTER)\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c-882-\t\t\treturn HRTIMER_NORESTART;\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c=909=int mac80211_hwsim_nan_start(struct ieee80211_hw *hw,\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c-926-\t\t/* Start in the \"scan\" phase and stay there for a little bit */\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c:927:\t\tdata-\u003enan.phase = MAC80211_HWSIM_NAN_PHASE_SCAN;\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c-928-\t\tdata-\u003enan.random_factor_valid_dwst = 1;\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c-930-\t\tdata-\u003enan.master_pref = conf-\u003emaster_pref;\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c:931:\t\tdata-\u003enan.role = MAC80211_HWSIM_NAN_ROLE_MASTER;\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c-932-\t\tmemset(\u0026data-\u003enan.current_ami, 0,\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c=1226=bool mac80211_hwsim_nan_receive(struct ieee80211_hw *hw,\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c-1235-\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c:1236:\tif (data-\u003enan.phase == MAC80211_HWSIM_NAN_PHASE_SCAN)\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.c-1237-\t\treturn channel-\u003ecenter_freq == 2437;\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.h-6-\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.h:7:#ifndef __MAC80211_HWSIM_NAN_H\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.h:8:#define __MAC80211_HWSIM_NAN_H\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.h-9-\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.h=10=enum mac80211_hwsim_nan_phase {\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.h:11:\tMAC80211_HWSIM_NAN_PHASE_SCAN,\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.h:12:\tMAC80211_HWSIM_NAN_PHASE_WARMUP,\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.h:13:\tMAC80211_HWSIM_NAN_PHASE_UP,\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.h-14-};\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.h=16=enum mac80211_hwsim_nan_role {\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.h:17:\tMAC80211_HWSIM_NAN_ROLE_MASTER,\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.h:18:\tMAC80211_HWSIM_NAN_ROLE_SYNC,\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.h:19:\tMAC80211_HWSIM_NAN_ROLE_NON_SYNC,\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.h-20-};\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.h=99=void mac80211_hwsim_nan_local_sched_changed(struct ieee80211_hw *hw,\n--\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.h-101-\ndrivers/net/wireless/virtual/mac80211_hwsim_nan.h:102:#endif /* __MAC80211_HWSIM_NAN_H */\n"
} |
| 7/2 | 2026/09/24 13:42 | llm | patch-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 72638 output: 36 thoughts: 39 Thoughts: Here's my take on the situation: **Investigating Kconfig Files for Wireless Network Configuration** Okay, so I need to get a handle on the configuration options related to wireless networking in the kernel. My initial thought is to zero in on the relevant Kconfig files. I'm thinking, based on past experience, that the configurations will most likely be situated within a few key areas of the kernel's source tree. I'll start by checking out `drivers/net/wireless/virtual/Kconfig`, that seems like a good place for drivers that might virtualize a wireless interface. Then, I'll need to go to the more general `net/wireless/Kconfig` directory to see if there are common configuration parameters for the wireless subsystem. Finally, since mac80211 is the standard wireless stack, `net/mac80211/Kconfig` is definitely where I expect to find the lower-level stuff. Let's start there and see what we can find. I anticipate a fair amount of overlap and cross-referencing between these files, so I'll be keeping an eye out for how they relate to each other. |
| 8/2 | 2026/09/24 13:42 | tool | read-file |
0mArgs:{
"File": "drivers/net/wireless/virtual/Kconfig",
"FirstLine": 1,
"LineCount": 50
}Results: {
"Contents": " 1:\tconfig MAC80211_HWSIM\n 2:\t\ttristate \"Simulated radio testing tool for mac80211\"\n 3:\t\tdepends on MAC80211\n 4:\t\thelp\n 5:\t\t This driver is a developer testing tool that can be used to test\n 6:\t\t IEEE 802.11 networking stack (mac80211) functionality. This is not\n 7:\t\t needed for normal wireless LAN usage and is only for testing. See\n 8:\t\t Documentation/networking/mac80211_hwsim for more information on how\n 9:\t\t to use this tool.\n 10:\t\n 11:\t\t To compile this driver as a module, choose M here: the module will be\n 12:\t\t called mac80211_hwsim. If unsure, say N.\n 13:\t\n 14:\tconfig VIRT_WIFI\n 15:\t\ttristate \"Wifi wrapper for ethernet drivers\"\n 16:\t\tdepends on CFG80211\n 17:\t\thelp\n 18:\t\t This option adds support for ethernet connections to appear as if they\n 19:\t\t are wifi connections through a special rtnetlink device.\n 20:\t\n"
} |
| 9/2 | 2026/09/24 13:42 | llm | patch-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 73282 output: 32 thoughts: 26 Thoughts: Here's my thought process, summarized as you requested: **Kconfig Dive Begins** Alright, time to dig into the configuration files. My immediate focus needs to be on the wireless and mac80211 components. I'm going to start by examining `net/wireless/Kconfig` and `net/mac80211/Kconfig`. This is standard operating procedure when working with these kernel modules; those Kconfig files are the heart of how these features are configured, enabled, and built. I need to understand what options are available, what dependencies exist, and how the various features are intertwined. Getting a solid grasp of the Kconfig structure is crucial before I can even begin to think about making any targeted changes or comprehending the build process for specific modules. This will give me a base to understand and then build upon the config that will best fit what I'm looking to achieve. |
| 10/2 | 2026/09/24 13:42 | tool | read-file |
0mArgs:{
"File": "net/wireless/Kconfig",
"FirstLine": 1,
"LineCount": 50
}Results: {
"Contents": " 1:\t# SPDX-License-Identifier: GPL-2.0-only\n 2:\tconfig WIRELESS_EXT\n 3:\t\tbool\n 4:\t\n 5:\tconfig WEXT_CORE\n 6:\t\tdef_bool y\n 7:\t\tdepends on CFG80211_WEXT || WIRELESS_EXT\n 8:\t\n 9:\tconfig WEXT_PROC\n 10:\t\tdef_bool y\n 11:\t\tdepends on PROC_FS\n 12:\t\tdepends on WEXT_CORE\n 13:\t\n 14:\tconfig WEXT_PRIV\n 15:\t\tbool\n 16:\t\n 17:\tconfig CFG80211\n 18:\t\ttristate \"cfg80211 - wireless configuration API\"\n 19:\t\tdepends on RFKILL || !RFKILL\n 20:\t\tselect FW_LOADER\n 21:\t\tselect CRC32\n 22:\t\t# may need to update this when certificates are changed and are\n 23:\t\t# using a different algorithm, though right now they shouldn't\n 24:\t\t# (this is here rather than below to allow it to be a module)\n 25:\t\tselect CRYPTO_SHA256 if CFG80211_USE_KERNEL_REGDB_KEYS\n 26:\t\thelp\n 27:\t\t cfg80211 is the Linux wireless LAN (802.11) configuration API.\n 28:\t\t Enable this if you have a wireless device.\n 29:\t\n 30:\t\t For more information refer to documentation on the wireless wiki:\n 31:\t\n 32:\t\t https://wireless.wiki.kernel.org/en/developers/Documentation/cfg80211\n 33:\t\n 34:\t\t When built as a module it will be called cfg80211.\n 35:\t\n 36:\tif CFG80211\n 37:\t\n 38:\tconfig NL80211_TESTMODE\n 39:\t\tbool \"nl80211 testmode command\"\n 40:\t\thelp\n 41:\t\t The nl80211 testmode command helps implementing things like\n 42:\t\t factory calibration or validation tools for wireless chips.\n 43:\t\n 44:\t\t Select this option ONLY for kernels that are specifically\n 45:\t\t built for such purposes.\n 46:\t\n 47:\t\t Debugging tools that are supposed to end up in the hands of\n 48:\t\t users should better be implemented with debugfs.\n 49:\t\n 50:\t\t Say N.\n"
} |
| 11/2 | 2026/09/24 13:42 | llm | patch-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 74197 output: 37 thoughts: 17 Thoughts: Here's a summary of my thought process, as requested: **Diving into `net/mac80211/Kconfig`** Okay, time to get started. My first instinct is to dive into the `net/mac80211/Kconfig` file. Given my expertise, I know this is where the core configuration options related to the mac80211 subsystem are defined. I expect to find the settings that govern driver inclusion, feature enable/disable switches, and potentially some default values for parameters related to wireless networking. My hope is to quickly identify the relevant configuration option(s) that are causing me issues. From experience, this is usually the best first step in understanding and potentially troubleshooting the current setup. I'll need to remember to pay close attention to any dependencies and default values declared there. This gives me a high level starting point from which to work. |
| 12/2 | 2026/09/24 13:42 | tool | read-file |
0mArgs:{
"File": "net/mac80211/Kconfig",
"FirstLine": 1,
"LineCount": 50
}Results: {
"Contents": " 1:\t# SPDX-License-Identifier: GPL-2.0-only\n 2:\tconfig MAC80211\n 3:\t\ttristate \"Generic IEEE 802.11 Networking Stack (mac80211)\"\n 4:\t\tdepends on CFG80211\n 5:\t\tselect CRYPTO\n 6:\t\tselect CRYPTO_LIB_AES_CBC_MACS\n 7:\t\tselect CRYPTO_LIB_ARC4\n 8:\t\tselect CRYPTO_AES\n 9:\t\tselect CRYPTO_CCM\n 10:\t\tselect CRYPTO_GCM\n 11:\t\tselect CRC32\n 12:\t\thelp\n 13:\t\t This option enables the hardware independent IEEE 802.11\n 14:\t\t networking stack.\n 15:\t\n 16:\tcomment \"CFG80211 needs to be enabled for MAC80211\"\n 17:\t\tdepends on CFG80211=n\n 18:\t\n 19:\tif MAC80211 != n\n 20:\t\n 21:\tconfig MAC80211_HAS_RC\n 22:\t\tbool\n 23:\t\n 24:\tconfig MAC80211_RC_MINSTREL\n 25:\t\tbool \"Minstrel\" if EXPERT\n 26:\t\tselect MAC80211_HAS_RC\n 27:\t\tdefault y\n 28:\t\thelp\n 29:\t\t This option enables the 'minstrel' TX rate control algorithm\n 30:\t\n 31:\tchoice\n 32:\t\tprompt \"Default rate control algorithm\"\n 33:\t\tdepends on MAC80211_HAS_RC\n 34:\t\tdefault MAC80211_RC_DEFAULT_MINSTREL\n 35:\t\thelp\n 36:\t\t This option selects the default rate control algorithm\n 37:\t\t mac80211 will use. Note that this default can still be\n 38:\t\t overridden through the ieee80211_default_rc_algo module\n 39:\t\t parameter if different algorithms are available.\n 40:\t\n 41:\tconfig MAC80211_RC_DEFAULT_MINSTREL\n 42:\t\tbool \"Minstrel\"\n 43:\t\tdepends on MAC80211_RC_MINSTREL\n 44:\t\thelp\n 45:\t\t Select Minstrel as the default rate control algorithm.\n 46:\t\n 47:\t\n 48:\tendchoice\n 49:\t\n 50:\tconfig MAC80211_RC_DEFAULT\n"
} |
| 13/2 | 2026/09/24 13:42 | llm | patch-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 75112 output: 37 thoughts: 19 Thoughts: Okay, here's my take on those thoughts, tailored for an expert audience: **Examining the Dependencies of mt7996 in Kconfig** Right, let's take a quick look at the `mt7996` Kconfig file. I need to be absolutely sure I understand its dependencies. I already have a pretty good mental map of the wireless subsystem and the broader kernel configuration, but I want to be *certain* that this module isn't unexpectedly pulling in anything that could cause conflicts or introduce unwanted baggage. I need to understand what other features or drivers it relies on. I'll need to know if I can enable this in isolation or if there are upstream implications that need to be considered. |
| 14/2 | 2026/09/24 13:42 | tool | grepper |
0mArgs:{
"Expression": "MT7996",
"PathPrefix": "drivers/net/wireless/mediatek/mt76"
}Results: {
"Output": "\nFull output is too long, showing 500 out of 1582 lines.\nUse more precise expression if possible.\n\ndrivers/net/wireless/mediatek/mt76/Makefile=50=obj-$(CONFIG_MT7921_COMMON) += mt7921/\ndrivers/net/wireless/mediatek/mt76/Makefile:51:obj-$(CONFIG_MT7996E) += mt7996/\ndrivers/net/wireless/mediatek/mt76/Makefile-52-obj-$(CONFIG_MT7925_COMMON) += mt7925/\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/Kconfig-1-# SPDX-License-Identifier: BSD-3-Clause-Clear\ndrivers/net/wireless/mediatek/mt76/mt7996/Kconfig:2:config MT7996E\ndrivers/net/wireless/mediatek/mt76/mt7996/Kconfig:3:\ttristate \"MediaTek MT7996 (PCIe) support\"\ndrivers/net/wireless/mediatek/mt76/mt7996/Kconfig-4-\tselect MT76_CONNAC_LIB\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/Kconfig-9-\thelp\ndrivers/net/wireless/mediatek/mt76/mt7996/Kconfig:10:\t This adds support for MT7996-based PCIe wireless devices,\ndrivers/net/wireless/mediatek/mt76/mt7996/Kconfig-11-\t which support concurrent tri-band operation at 6GHz, 5GHz,\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/Kconfig-15-\ndrivers/net/wireless/mediatek/mt76/mt7996/Kconfig:16:config MT7996_NPU\ndrivers/net/wireless/mediatek/mt76/mt7996/Kconfig:17:\tbool \"MT7996 (PCIe) NPU support\"\ndrivers/net/wireless/mediatek/mt76/mt7996/Kconfig:18:\tdepends on MT7996E\ndrivers/net/wireless/mediatek/mt76/mt7996/Kconfig-19-\tdepends on NET_AIROHA_NPU=y || MT76_CORE=NET_AIROHA_NPU\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/Makefile-2-\ndrivers/net/wireless/mediatek/mt76/mt7996/Makefile:3:obj-$(CONFIG_MT7996E) += mt7996e.o\ndrivers/net/wireless/mediatek/mt76/mt7996/Makefile-4-\ndrivers/net/wireless/mediatek/mt76/mt7996/Makefile=5=mt7996e-y := pci.o init.o dma.o eeprom.o main.o mcu.o mac.o \\\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/Makefile-7-\ndrivers/net/wireless/mediatek/mt76/mt7996/Makefile:8:mt7996e-$(CONFIG_MT7996_NPU) += npu.o\ndrivers/net/wireless/mediatek/mt76/mt7996/Makefile-9-mt7996e-$(CONFIG_DEV_COREDUMP) += coredump.o\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/coredump.c=48=mt7996_coredump_get_mem_layout(struct mt7996_dev *dev, u32 *num)\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/coredump.c-50-\tswitch (mt76_chip(\u0026dev-\u003emt76)) {\ndrivers/net/wireless/mediatek/mt76/mt7996/coredump.c:51:\tcase MT7996_DEVICE_ID:\ndrivers/net/wireless/mediatek/mt76/mt7996/coredump.c:52:\tcase MT7996_DEVICE_ID_2:\ndrivers/net/wireless/mediatek/mt76/mt7996/coredump.c-53-\t\t*num = ARRAY_SIZE(mt7996_mem_regions);\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c=46=static void mt7996_dma_config(struct mt7996_dev *dev)\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-59-\t/* rx queue */\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:60:\tRXQ_CONFIG(MT_RXQ_MCU, WFDMA0, MT_INT_RX_DONE_WM, MT7996_RXQ_MCU_WM);\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-61-\t/* for mt7990, RX ring 1 is for SDO instead */\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:62:\tRXQ_CONFIG(MT_RXQ_MCU_WA, WFDMA0, MT_INT_RX_DONE_WA, MT7996_RXQ_MCU_WA);\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:63:\tRXQ_CONFIG(MT_RXQ_MAIN, WFDMA0, MT_INT_RX_DONE_BAND0, MT7996_RXQ_BAND0);\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-64-\tif (mt7996_has_wa(dev))\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-65-\t\tRXQ_CONFIG(MT_RXQ_MAIN_WA, WFDMA0, MT_INT_RX_DONE_WA_MAIN,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:66:\t\t\t MT7996_RXQ_MCU_WA_MAIN);\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-67-\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-69-\tcase MT7992_DEVICE_ID:\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:70:\t\tRXQ_CONFIG(MT_RXQ_BAND1_WA, WFDMA0, MT_INT_RX_DONE_WA_EXT, MT7996_RXQ_MCU_WA_EXT);\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:71:\t\tRXQ_CONFIG(MT_RXQ_BAND1, WFDMA0, MT_INT_RX_DONE_BAND1, MT7996_RXQ_BAND1);\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-72-\t\tbreak;\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-73-\tcase MT7990_DEVICE_ID:\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:74:\t\tRXQ_CONFIG(MT_RXQ_BAND1, WFDMA0, MT_INT_RX_DONE_BAND1, MT7996_RXQ_BAND1);\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-75-\t\tRXQ_CONFIG(MT_RXQ_TXFREE_BAND0, WFDMA0,\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-80-\t\tbreak;\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:81:\tcase MT7996_DEVICE_ID:\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-82-\tdefault:\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-83-\t\t/* mt7996 band2 */\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:84:\t\tRXQ_CONFIG(MT_RXQ_BAND2_WA, WFDMA0, MT_INT_RX_DONE_WA_TRI, MT7996_RXQ_MCU_WA_TRI);\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:85:\t\tRXQ_CONFIG(MT_RXQ_BAND2, WFDMA0, MT_INT_RX_DONE_BAND2, MT7996_RXQ_BAND2);\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-86-\t\tbreak;\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-91-\t\tRXQ_CONFIG(MT_RXQ_RRO_BAND0, WFDMA0, MT_INT_RX_DONE_RRO_BAND0,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:92:\t\t\t MT7996_RXQ_RRO_BAND0);\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-93-\t\tif (dev-\u003emt76.hwrro_mode == MT76_HWRRO_V3)\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-95-\t\t\t\t MT_INT_RX_DONE_MSDU_PG_BAND0,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:96:\t\t\t\t MT7996_RXQ_MSDU_PG_BAND0);\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-97-\t\tif (is_mt7996(\u0026dev-\u003emt76)) {\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-98-\t\t\tRXQ_CONFIG(MT_RXQ_TXFREE_BAND0, WFDMA0,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:99:\t\t\t\t MT_INT_RX_TXFREE_MAIN, MT7996_RXQ_TXFREE0);\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-100-\t\t\t/* band1 */\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-102-\t\t\t\t MT_INT_RX_DONE_MSDU_PG_BAND1,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:103:\t\t\t\t MT7996_RXQ_MSDU_PG_BAND1);\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-104-\t\t\t/* band2 */\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-106-\t\t\t\t MT_INT_RX_DONE_RRO_BAND2,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:107:\t\t\t\t MT7996_RXQ_RRO_BAND2);\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-108-\t\t\tRXQ_CONFIG(MT_RXQ_MSDU_PAGE_BAND2, WFDMA0,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-109-\t\t\t\t MT_INT_RX_DONE_MSDU_PG_BAND2,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:110:\t\t\t\t MT7996_RXQ_MSDU_PG_BAND2);\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-111-\t\t\tRXQ_CONFIG(MT_RXQ_TXFREE_BAND2, WFDMA0,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:112:\t\t\t\t MT_INT_RX_TXFREE_TRI, MT7996_RXQ_TXFREE2);\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-113-\t\t} else {\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-115-\t\t\t\t MT_INT_RX_DONE_RRO_BAND1,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:116:\t\t\t\t MT7996_RXQ_RRO_BAND1);\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-117-\t\t}\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-121-\t\t\t\t MT_INT_RX_DONE_RRO_IND,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:122:\t\t\t\t MT7996_RXQ_RRO_IND);\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-123-\t\telse\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-125-\t\t\t\t MT_INT_RX_DONE_RRO_RXDMAD_C,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:126:\t\t\t\t MT7996_RXQ_RRO_RXDMAD_C);\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-127-\t}\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-133-\t\t\t\tTXQ_CONFIG(0, WFDMA0, MT_INT_TX_DONE_BAND2,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:134:\t\t\t\t\t MT7996_TXQ_BAND2);\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-135-\t\t\t\tTXQ_CONFIG(1, WFDMA0, MT_INT_TX_DONE_BAND0,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:136:\t\t\t\t\t MT7996_TXQ_BAND0);\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-137-\t\t\t\tTXQ_CONFIG(2, WFDMA0, MT_INT_TX_DONE_BAND1,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:138:\t\t\t\t\t MT7996_TXQ_BAND1);\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-139-\t\t\t} else {\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-141-\t\t\t\tTXQ_CONFIG(0, WFDMA0, MT_INT_TX_DONE_BAND0,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:142:\t\t\t\t\t MT7996_TXQ_BAND0);\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-143-\t\t\t\tTXQ_CONFIG(1, WFDMA0, MT_INT_TX_DONE_BAND1,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:144:\t\t\t\t\t MT7996_TXQ_BAND1);\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-145-\t\t\t\tTXQ_CONFIG(2, WFDMA0, MT_INT_TX_DONE_BAND2,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:146:\t\t\t\t\t MT7996_TXQ_BAND2);\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-147-\t\t\t}\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-150-\t\t\tTXQ_CONFIG(0, WFDMA0, MT_INT_TX_DONE_BAND0,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:151:\t\t\t\t MT7996_TXQ_BAND0);\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-152-\t\t\tTXQ_CONFIG(2, WFDMA0, MT_INT_TX_DONE_BAND1,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:153:\t\t\t\t MT7996_TXQ_BAND1);\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-154-\t\t}\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-158-\t\t\tTXQ_CONFIG(0, WFDMA0, MT_INT_TX_DONE_BAND0,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:159:\t\t\t\t MT7996_TXQ_BAND0);\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-160-\t\t\tTXQ_CONFIG(1, WFDMA0, MT_INT_TX_DONE_BAND2,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:161:\t\t\t\t MT7996_TXQ_BAND2);\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-162-\t\t} else {\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-164-\t\t\tTXQ_CONFIG(0, WFDMA0, MT_INT_TX_DONE_BAND0,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:165:\t\t\t\t MT7996_TXQ_BAND0);\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-166-\t\t\tTXQ_CONFIG(1, WFDMA0, MT_INT_TX_DONE_BAND1,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:167:\t\t\t\t MT7996_TXQ_BAND1);\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-168-\t\t}\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-171-\t/* mcu tx queue */\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:172:\tMCUQ_CONFIG(MT_MCUQ_FWDL, WFDMA0, MT_INT_TX_DONE_FWDL, MT7996_TXQ_FWDL);\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:173:\tMCUQ_CONFIG(MT_MCUQ_WM, WFDMA0, MT_INT_TX_DONE_MCU_WM, MT7996_TXQ_MCU_WM);\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-174-\tif (mt7996_has_wa(dev))\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-175-\t\tMCUQ_CONFIG(MT_MCUQ_WA, WFDMA0, MT_INT_TX_DONE_MCU_WA,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:176:\t\t\t MT7996_TXQ_MCU_WA);\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-177-}\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c=521=int mt7996_dma_rro_init(struct mt7996_dev *dev)\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-535-\t\t\t\t MT_RXQ_ID(MT_RXQ_RRO_RXDMAD_C),\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:536:\t\t\t\t MT7996_RX_RING_SIZE,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:537:\t\t\t\t MT7996_RX_BUF_SIZE,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-538-\t\t\t\t MT_RXQ_RRO_AP_RING_BASE);\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-561-\t\t\t MT_RXQ_ID(MT_RXQ_RRO_IND),\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:562:\t\t\t MT7996_RX_RING_SIZE,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-563-\t\t\t 0, MT_RXQ_RRO_IND_RING_BASE);\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-574-\tsize = is_mt7996(mdev) \u0026\u0026 mt76_npu_device_active(mdev)\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:575:\t ? MT7996_NPU_RX_RING_SIZE / 4 : MT7996_RX_RING_SIZE;\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-576-\tret = mt76_queue_alloc(dev, \u0026mdev-\u003eq_rx[MT_RXQ_MSDU_PAGE_BAND0],\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-577-\t\t\t MT_RXQ_ID(MT_RXQ_MSDU_PAGE_BAND0),\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:578:\t\t\t size, MT7996_RX_MSDU_PAGE_SIZE,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-579-\t\t\t MT_RXQ_RING_BASE(MT_RXQ_MSDU_PAGE_BAND0));\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-591-\t\tsize = is_mt7996(mdev) \u0026\u0026 mt76_npu_device_active(mdev)\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:592:\t\t ? MT7996_NPU_RX_RING_SIZE / 2 : MT7996_RX_RING_SIZE;\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-593-\t\tret = mt76_queue_alloc(dev, \u0026mdev-\u003eq_rx[MT_RXQ_MSDU_PAGE_BAND1],\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-594-\t\t\t\t MT_RXQ_ID(MT_RXQ_MSDU_PAGE_BAND1),\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:595:\t\t\t\t size, MT7996_RX_MSDU_PAGE_SIZE,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-596-\t\t\t\t MT_RXQ_RING_BASE(MT_RXQ_MSDU_PAGE_BAND1));\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-609-\t\tsize = is_mt7996(mdev) \u0026\u0026 mt76_npu_device_active(mdev)\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:610:\t\t ? MT7996_NPU_RX_RING_SIZE : MT7996_RX_RING_SIZE;\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-611-\t\tret = mt76_queue_alloc(dev, \u0026mdev-\u003eq_rx[MT_RXQ_MSDU_PAGE_BAND2],\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-612-\t\t\t\t MT_RXQ_ID(MT_RXQ_MSDU_PAGE_BAND2),\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:613:\t\t\t\t size, MT7996_RX_MSDU_PAGE_SIZE,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-614-\t\t\t\t MT_RXQ_RING_BASE(MT_RXQ_MSDU_PAGE_BAND2));\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c=664=int mt7996_dma_init(struct mt7996_dev *dev)\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-683-\t\tret = mt7996_init_tx_queues(\u0026dev-\u003ephy, MT_TXQ_ID(0),\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:684:\t\t\t\t\t MT7996_NPU_TX_RING_SIZE,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-685-\t\t\t\t\t MT_TXQ_RING_BASE(0) + hif1_ofs,\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-689-\t\t\t\t\t MT_TXQ_ID(dev-\u003emphy.band_idx),\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:690:\t\t\t\t\t MT7996_TX_RING_SIZE,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-691-\t\t\t\t\t MT_TXQ_RING_BASE(0), wed);\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-697-\t\t\t\t MT_MCUQ_ID(MT_MCUQ_WM),\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:698:\t\t\t\t MT7996_TX_MCU_RING_SIZE,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-699-\t\t\t\t MT_MCUQ_RING_BASE(MT_MCUQ_WM));\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-706-\t\t\t\t\t MT_MCUQ_ID(MT_MCUQ_WA),\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:707:\t\t\t\t\t MT7996_TX_MCU_RING_SIZE,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-708-\t\t\t\t\t MT_MCUQ_RING_BASE(MT_MCUQ_WA));\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-715-\t\t\t\t MT_MCUQ_ID(MT_MCUQ_FWDL),\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:716:\t\t\t\t MT7996_TX_FWDL_RING_SIZE,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-717-\t\t\t\t MT_MCUQ_RING_BASE(MT_MCUQ_FWDL));\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-723-\t\t\t MT_RXQ_ID(MT_RXQ_MCU),\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:724:\t\t\t MT7996_RX_MCU_RING_SIZE,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:725:\t\t\t MT7996_RX_MCU_BUF_SIZE,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-726-\t\t\t MT_RXQ_RING_BASE(MT_RXQ_MCU));\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-732-\t\t\t MT_RXQ_ID(MT_RXQ_MCU_WA),\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:733:\t\t\t MT7996_RX_MCU_RING_SIZE_WA,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:734:\t\t\t MT7996_RX_MCU_BUF_SIZE,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-735-\t\t\t MT_RXQ_RING_BASE(MT_RXQ_MCU_WA));\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-746-\t\t\t MT_RXQ_ID(MT_RXQ_MAIN),\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:747:\t\t\t MT7996_RX_RING_SIZE,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-748-\t\t\t MT_RX_BUF_SIZE,\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-766-\t\t\t\t MT_RXQ_ID(MT_RXQ_MAIN_WA),\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:767:\t\t\t\t MT7996_RX_MCU_RING_SIZE,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-768-\t\t\t\t MT_RX_BUF_SIZE,\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-778-\t\t\t\t MT_RXQ_ID(MT_RXQ_TXFREE_BAND0),\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:779:\t\t\t\t MT7996_RX_MCU_RING_SIZE,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:780:\t\t\t\t MT7996_RX_BUF_SIZE,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-781-\t\t\t\t MT_RXQ_RING_BASE(MT_RXQ_TXFREE_BAND0));\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-792-\t\t\t\t MT_RXQ_ID(MT_RXQ_TXFREE_BAND1),\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:793:\t\t\t\t MT7996_RX_MCU_RING_SIZE,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:794:\t\t\t\t MT7996_RX_BUF_SIZE,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-795-\t\t\t\t MT_RXQ_RING_BASE(MT_RXQ_TXFREE_BAND1));\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-804-\t\t\t\t MT_RXQ_ID(MT_RXQ_BAND2),\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:805:\t\t\t\t MT7996_RX_RING_SIZE,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-806-\t\t\t\t MT_RX_BUF_SIZE,\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-820-\t\t\t\t MT_RXQ_ID(MT_RXQ_BAND2_WA),\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:821:\t\t\t\t MT7996_RX_MCU_RING_SIZE,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-822-\t\t\t\t MT_RX_BUF_SIZE,\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-835-\t\t\t\t MT_RXQ_ID(MT_RXQ_BAND1),\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:836:\t\t\t\t MT7996_RX_RING_SIZE,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-837-\t\t\t\t MT_RX_BUF_SIZE,\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-852-\t\t\t\t\t MT_RXQ_ID(MT_RXQ_BAND1_WA),\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:853:\t\t\t\t\t MT7996_RX_MCU_RING_SIZE,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-854-\t\t\t\t\t MT_RX_BUF_SIZE,\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-868-\t\t\t\t MT_RXQ_ID(MT_RXQ_RRO_BAND0),\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:869:\t\t\t\t MT7996_RX_RING_SIZE,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:870:\t\t\t\t MT7996_RX_BUF_SIZE,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-871-\t\t\t\t MT_RXQ_RING_BASE(MT_RXQ_RRO_BAND0));\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-883-\t\t\t\t\t MT_RXQ_ID(MT_RXQ_RRO_BAND1),\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:884:\t\t\t\t\t MT7996_RX_RING_SIZE,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:885:\t\t\t\t\t MT7996_RX_BUF_SIZE,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-886-\t\t\t\t\t MT_RXQ_RING_BASE(MT_RXQ_RRO_BAND1) + hif1_ofs);\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-900-\t\t\t\t\t MT_RXQ_ID(MT_RXQ_TXFREE_BAND0),\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:901:\t\t\t\t\t MT7996_RX_MCU_RING_SIZE,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:902:\t\t\t\t\t MT7996_RX_BUF_SIZE,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-903-\t\t\t\t\t MT_RXQ_RING_BASE(MT_RXQ_TXFREE_BAND0));\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-919-\t\t\t mt76_npu_device_active(\u0026dev-\u003emt76)\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:920:\t\t\t ? MT7996_NPU_RX_RING_SIZE : MT7996_RX_RING_SIZE;\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-921-\t\t\tret = mt76_queue_alloc(dev, \u0026dev-\u003emt76.q_rx[MT_RXQ_RRO_BAND2],\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-922-\t\t\t\t\t MT_RXQ_ID(MT_RXQ_RRO_BAND2),\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:923:\t\t\t\t\t size, MT7996_RX_BUF_SIZE,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-924-\t\t\t\t\t MT_RXQ_RING_BASE(MT_RXQ_RRO_BAND2) + hif1_ofs);\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-934-\t\t\t\t\t MT_RXQ_ID(MT_RXQ_TXFREE_BAND2),\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:935:\t\t\t\t\t MT7996_RX_MCU_RING_SIZE,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c:936:\t\t\t\t\t MT7996_RX_BUF_SIZE,\ndrivers/net/wireless/mediatek/mt76/mt7996/dma.c-937-\t\t\t\t\t MT_RXQ_RING_BASE(MT_RXQ_TXFREE_BAND2) + hif1_ofs);\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c=10=static int mt7996_check_eeprom(struct mt7996_dev *dev)\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-15-\tswitch (val) {\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c:16:\tcase MT7996_DEVICE_ID:\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-17-\t\treturn is_mt7996(\u0026dev-\u003emt76) ? 0 : -EINVAL;\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c=27=static char *mt7996_eeprom_name(struct mt7996_dev *dev)\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-32-\t\tcase MT7992_VAR_TYPE_23:\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c:33:\t\t\tif (dev-\u003evar.fem == MT7996_FEM_INT)\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-34-\t\t\t\treturn MT7992_EEPROM_DEFAULT_23_INT;\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-39-\t\tdefault:\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c:40:\t\t\tif (dev-\u003evar.fem == MT7996_FEM_INT)\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-41-\t\t\t\treturn MT7992_EEPROM_DEFAULT_INT;\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c:42:\t\t\tif (dev-\u003evar.fem == MT7996_FEM_MIX)\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-43-\t\t\t\treturn MT7992_EEPROM_DEFAULT_MIX;\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-46-\tcase MT7990_DEVICE_ID:\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c:47:\t\tif (dev-\u003evar.fem == MT7996_FEM_INT)\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-48-\t\t\treturn MT7990_EEPROM_DEFAULT_INT;\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-49-\t\treturn MT7990_EEPROM_DEFAULT;\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c:50:\tcase MT7996_DEVICE_ID:\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-51-\tdefault:\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-52-\t\tswitch (dev-\u003evar.type) {\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c:53:\t\tcase MT7996_VAR_TYPE_233:\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c:54:\t\t\tif (dev-\u003evar.fem == MT7996_FEM_INT)\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c:55:\t\t\t\treturn MT7996_EEPROM_DEFAULT_233_INT;\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c:56:\t\t\treturn MT7996_EEPROM_DEFAULT_233;\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c:57:\t\tcase MT7996_VAR_TYPE_444:\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-58-\t\tdefault:\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c:59:\t\t\tif (dev-\u003evar.fem == MT7996_FEM_INT)\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c:60:\t\t\t\treturn MT7996_EEPROM_DEFAULT_INT;\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c:61:\t\t\treturn MT7996_EEPROM_DEFAULT;\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-62-\t\t}\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c=98=static bool mt7996_eeprom_variant_valid(struct mt7996_dev *dev, const u8 *def)\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-108-\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c:109:\tif (dev-\u003evar.fem == MT7996_FEM_EXT \u0026\u0026\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-110-\t !(fem[0] == FEM_EXT \u0026\u0026 fem[1] == FEM_EXT))\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-111-\t\treturn false;\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c:112:\telse if (dev-\u003evar.fem == MT7996_FEM_INT \u0026\u0026\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-113-\t\t !(fem[0] == FEM_INT \u0026\u0026 fem[1] == FEM_INT))\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-114-\t\treturn false;\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c:115:\telse if (dev-\u003evar.fem == MT7996_FEM_MIX \u0026\u0026\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-116-\t\t !(fem[0] == FEM_INT \u0026\u0026 fem[1] == FEM_EXT))\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c=137=mt7996_eeprom_check_or_use_default(struct mt7996_dev *dev, bool use_default)\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-152-\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c:153:\tif (fw-\u003esize \u003c MT7996_EEPROM_SIZE) {\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-154-\t\tdev_err(dev-\u003emt76.dev, \"Invalid default bin size\\n\");\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-162-\tdev_warn(dev-\u003emt76.dev, \"eeprom load fail, use default bin\\n\");\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c:163:\tmemcpy(eeprom, fw-\u003edata, MT7996_EEPROM_SIZE);\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-164-\tdev-\u003eeeprom_mode = EEPROM_MODE_DEFAULT_BIN;\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c=172=static int mt7996_eeprom_load(struct mt7996_dev *dev)\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-177-\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c:178:\tret = mt76_eeprom_init(\u0026dev-\u003emt76, MT7996_EEPROM_SIZE);\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-179-\tif (ret \u003c 0)\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-186-\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c:187:\tmemset(dev-\u003emt76.eeprom.data, 0, MT7996_EEPROM_SIZE);\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-188-\tif (mt7996_has_ext_eeprom(dev)) {\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-190-\t\tdev-\u003eeeprom_mode = EEPROM_MODE_EXT;\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c:191:\t\teeprom_blk_size = MT7996_EXT_EEPROM_BLOCK_SIZE;\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-192-\t} else {\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-196-\t\tdev-\u003eeeprom_mode = EEPROM_MODE_EFUSE;\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c:197:\t\teeprom_blk_size = MT7996_EEPROM_BLOCK_SIZE;\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-198-\t\tret = mt7996_mcu_get_efuse_free_block(dev, \u0026free_block_num);\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-217-\t/* read eeprom data from fw */\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c:218:\tblock_num = DIV_ROUND_UP(MT7996_EEPROM_SIZE, eeprom_blk_size);\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-219-\tfor (i = 1; i \u003c block_num; i++) {\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-222-\t\tif (i == block_num - 1)\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c:223:\t\t\tlen = MT7996_EEPROM_SIZE % eeprom_blk_size;\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-224-\t\tret = mt7996_mcu_get_eeprom(dev, i * eeprom_blk_size,\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c=395=bool mt7996_eeprom_has_background_radar(struct mt7996_dev *dev)\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-397-\tswitch (mt76_chip(\u0026dev-\u003emt76)) {\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c:398:\tcase MT7996_DEVICE_ID:\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c:399:\t\tif (dev-\u003evar.type == MT7996_VAR_TYPE_233)\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.c-400-\t\t\treturn false;\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.h-5-\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.h:6:#ifndef __MT7996_EEPROM_H\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.h:7:#define __MT7996_EEPROM_H\ndrivers/net/wireless/mediatek/mt76/mt7996/eeprom.h-8-\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c=17=static const struct ieee80211_iface_limit if_limits_global = {\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c:18:\t.max = MT7996_MAX_INTERFACES * MT7996_MAX_RADIOS,\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-19-\t.types = BIT(NL80211_IFTYPE_STATION)\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c=27=static const struct ieee80211_iface_combination if_comb_global = {\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-29-\t.n_limits = 1,\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c:30:\t.max_interfaces = MT7996_MAX_INTERFACES * MT7996_MAX_RADIOS,\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c:31:\t.num_different_channels = MT7996_MAX_RADIOS,\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-32-\t.radar_detect_widths = BIT(NL80211_CHAN_WIDTH_20_NOHT) |\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c=39=static const struct ieee80211_iface_combination if_comb_global_7992 = {\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-42-\t.max_interfaces = 32,\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c:43:\t.num_different_channels = MT7996_MAX_RADIOS - 1,\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-44-\t.radar_detect_widths = BIT(NL80211_CHAN_WIDTH_20_NOHT) |\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c=51=static const struct ieee80211_iface_limit if_limits[] = {\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-58-\t}, {\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c:59:\t\t.max = MT7996_MAX_INTERFACES,\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-60-\t\t.types = BIT(NL80211_IFTYPE_STATION)\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c=64=static const struct ieee80211_iface_combination if_comb = {\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-66-\t.n_limits = ARRAY_SIZE(if_limits),\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c:67:\t.max_interfaces = MT7996_MAX_INTERFACES,\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-68-\t.num_different_channels = 1,\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c=84=static const struct wiphy_iftype_ext_capab iftypes_ext_capa[] = {\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-92-\t\t\tFIELD_PREP_CONST(IEEE80211_MLD_CAP_OP_MAX_SIMUL_LINKS,\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c:93:\t\t\t\t\t MT7996_MAX_RADIOS - 1),\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-94-\t}, {\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-101-\t\t\tFIELD_PREP_CONST(IEEE80211_MLD_CAP_OP_MAX_SIMUL_LINKS,\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c:102:\t\t\t\t\t MT7996_MAX_RADIOS - 1),\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-103-\t},\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c=132=static ssize_t mt7996_thermal_temp_store(struct device *dev,\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-147-\t/* add a safety margin ~10 */\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c:148:\tif ((i - 1 == MT7996_CRIT_TEMP_IDX \u0026\u0026\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c:149:\t val \u003e phy-\u003ethrottle_temp[MT7996_MAX_TEMP_IDX] - 10) ||\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c:150:\t (i - 1 == MT7996_MAX_TEMP_IDX \u0026\u0026\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c:151:\t val - 10 \u003c phy-\u003ethrottle_temp[MT7996_CRIT_TEMP_IDX])) {\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-152-\t\tdev_err(phy-\u003edev-\u003emt76.dev,\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c=183=mt7996_thermal_get_max_throttle_state(struct thermal_cooling_device *cdev,\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-185-{\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c:186:\t*state = MT7996_CDEV_THROTTLE_MAX;\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-187-\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c=203=mt7996_thermal_set_cur_throttle_state(struct thermal_cooling_device *cdev,\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-206-\tstruct mt7996_phy *phy = cdev-\u003edevdata;\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c:207:\tu8 throttling = MT7996_THERMAL_THROTTLE_MAX - state;\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-208-\tint ret;\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-209-\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c:210:\tif (state \u003e MT7996_CDEV_THROTTLE_MAX) {\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-211-\t\tdev_err(phy-\u003edev-\u003emt76.dev,\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c=250=static int mt7996_thermal_init(struct mt7996_phy *phy)\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-274-\t/* initialize critical/maximum high temperature */\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c:275:\tphy-\u003ethrottle_temp[MT7996_CRIT_TEMP_IDX] = MT7996_CRIT_TEMP;\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c:276:\tphy-\u003ethrottle_temp[MT7996_MAX_TEMP_IDX] = MT7996_MAX_TEMP;\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-277-\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c=605=static void mt7996_mac_init_basic_rates(struct mt7996_dev *dev)\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-611-\t\t/* odd index for driver, even index for firmware */\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c:612:\t\tu16 idx = MT7996_BASIC_RATES_TBL + 2 * i;\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-613-\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c=692=static int mt7996_register_phy(struct mt7996_dev *dev, enum mt76_band_id band)\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-755-\t\t\t mt76_npu_device_active(\u0026dev-\u003emt76)\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c:756:\t\t\t ? MT7996_NPU_TX_RING_SIZE / 2 : MT7996_TX_RING_SIZE;\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-757-\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c=835=static void mt7996_rro_hw_init_v3(struct mt7996_dev *dev)\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-845-\t\twed-\u003ewlan.ind_cmd.win_size =\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c:846:\t\t\tffs(MT7996_RRO_WINDOW_MAX_LEN) - 6;\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-847-\t\tif (is_mt7996(\u0026dev-\u003emt76))\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-848-\t\t\twed-\u003ewlan.ind_cmd.particular_sid =\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c:849:\t\t\t\tMT7996_RRO_MAX_SESSION;\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-850-\t\telse\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-853-\t\t\tdev-\u003ewed_rro.session.phy_addr;\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c:854:\t\twed-\u003ewlan.ind_cmd.se_group_nums = MT7996_RRO_ADDR_ELEM_LEN;\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-855-\t\twed-\u003ewlan.ind_cmd.ack_sn_addr = MT_RRO_ACK_SN_CTRL;\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-871-\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c:872:\tsession_id = is_mt7996(\u0026dev-\u003emt76) ? MT7996_RRO_MAX_SESSION : 1;\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-873-\tmt76_wr(dev, MT_RRO_PARTICULAR_CFG1,\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c=959=static int mt7996_wed_rro_init(struct mt7996_dev *dev)\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-971-\t\t\tptr = dmam_alloc_coherent(dev-\u003emt76.dma_dev,\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c:972:\t\t\t\t\t\t MT7996_RRO_BA_BITMAP_CR_SIZE,\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-973-\t\t\t\t\t\t \u0026dev-\u003ewed_rro.ba_bitmap[i].phy_addr,\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-985-\t\tptr = dmam_alloc_coherent(dev-\u003emt76.dma_dev,\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c:986:\t\t\t\tMT7996_RRO_WINDOW_MAX_SIZE * sizeof(*addr),\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-987-\t\t\t\t\u0026dev-\u003ewed_rro.addr_elem[i].phy_addr,\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-993-\t\tmemset(dev-\u003ewed_rro.addr_elem[i].ptr, 0,\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c:994:\t\t MT7996_RRO_WINDOW_MAX_SIZE * sizeof(*addr));\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-995-\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-996-\t\taddr = dev-\u003ewed_rro.addr_elem[i].ptr;\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c:997:\t\tfor (j = 0; j \u003c MT7996_RRO_WINDOW_MAX_SIZE; j++) {\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-998-\t\t\taddr-\u003edata = cpu_to_le32(val);\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-1020-\t\tptr = dmam_alloc_coherent(dev-\u003emt76.dma_dev,\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c:1021:\t\t\t\t\t MT7996_RRO_MSDU_PG_SIZE_PER_CR,\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-1022-\t\t\t\t\t \u0026dev-\u003ewed_rro.msdu_pg[i].phy_addr,\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-1029-\t\tmemset(dev-\u003ewed_rro.msdu_pg[i].ptr, 0,\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c:1030:\t\t MT7996_RRO_MSDU_PG_SIZE_PER_CR);\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-1031-\t}\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-1054-\tptr = dmam_alloc_coherent(dev-\u003emt76.dma_dev,\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c:1055:\t\t\t\t MT7996_RRO_WINDOW_MAX_LEN * sizeof(*addr),\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-1056-\t\t\t\t \u0026dev-\u003ewed_rro.session.phy_addr,\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-1062-\taddr = dev-\u003ewed_rro.session.ptr;\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c:1063:\tfor (i = 0; i \u003c MT7996_RRO_WINDOW_MAX_LEN; i++) {\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-1064-\t\taddr-\u003edata = cpu_to_le32(val);\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c=1077=static void mt7996_wed_rro_free(struct mt7996_dev *dev)\n--\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-1088-\t\tdmam_free_coherent(dev-\u003emt76.dma_dev,\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c:1089:\t\t\t\t MT7996_RRO_BA_BITMAP_CR_SIZE,\ndrivers/net/wireless/mediatek/mt76/mt7996/init.c-1090-\t\t\t\t dev-\u003ewed_rro.ba_bitmap[i].ptr,\n--\n\n"
} |
| 15/2 | 2026/09/24 13:42 | llm | patch-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 93499 output: 36 thoughts: 945 Thoughts: **WorthFuzzing Evaluation**
Okay, let's break this down systematically. I see that MT7996E is a specific hardware driver, and `mac80211_hwsim` is virtual. `net/wireless` corresponds to cfg80211, `net/mac80211` is mac80211, and `mac80211_hwsim` is, well, `mac80211_hwsim`. And MT7996 is MT7996E.
Now, applying the WorthFuzzing criteria, specifically sections 2 and 3, let's see where these patches fall. Section 2 gives the negative criteria. It outlines scenarios where we'd *not* fuzz. Section 3 defines the positive cases.
Looking at the patch modifications:
1. `net/wireless/nl80211.c`, `net/wireless/scs.c`, `net/wireless/core.c` are all part of cfg80211, the core subsystem, which is accessible from userspace. Generic netlink (`NL80211_CMD_SET_SCS`, `NL80211_CMD_SET_MSCS`) exposes this.
2. `net/mac80211/scs.c`, `net/mac80211/rx.c`, `net/mac80211/wme.c`, `net/mac80211/iface.c`, `net/mac80211/debugfs_sta.c` touch the mac80211 wireless stack. Accessible via nl80211 and virtual wifi, thanks to `mac80211_hwsim`.
3. `drivers/net/wireless/virtual/mac80211_hwsim_main.c` is the virtual driver itself. It's emulated in software, totally accessible.
4. `drivers/net/wireless/mediatek/mt76/mt7996/` is the physical PCIe driver.
Since the patch series modifies reachable executable code within the cfg80211 and mac80211 core subsystems and also the virtual `mac80211_hwsim` driver, according to section 3, the `WorthFuzzing` value is unquestionably `true`. No argument there.
Next, let's move on to the `EnableConfigs`. We need to determine the necessary configurations for compiling and testing this code. I need to check `net/wireless/Kconfig`, `net/mac80211/Kconfig`, and `drivers/net/wireless/virtual/Kconfig`. It's going to need `CFG80211`, `MAC80211`, and `MAC80211_HWSIM`. The presence of debugfs is also important here.
I see `debugfs_create_u16("scs_status", 0600, data->debugfs, &data->scs_status);` and `DEBUGFS_ADD(scs);` and `DEBUGFS_ADD(mscs);` calls in the driver and stack respectively, so this relies on `MAC80211_DEBUGFS`. Let me confirm that `MAC80211_DEBUGFS` is in `net/mac80211/Kconfig`.
|
| 16/2 | 2026/09/24 13:42 | tool | grepper |
0mArgs:{
"Expression": "MAC80211_DEBUGFS",
"PathPrefix": "net/mac80211"
}Results: {
"Output": "net/mac80211/Kconfig=79=config MAC80211_LEDS\n--\nnet/mac80211/Kconfig-87-\nnet/mac80211/Kconfig:88:config MAC80211_DEBUGFS\nnet/mac80211/Kconfig-89-\tbool \"Export mac80211 internals in DebugFS\"\n--\nnet/mac80211/Kconfig=293=config MAC80211_DEBUG_COUNTERS\n--\nnet/mac80211/Kconfig-295-\tdepends on MAC80211_DEBUG_MENU\nnet/mac80211/Kconfig:296:\tdepends on MAC80211_DEBUGFS\nnet/mac80211/Kconfig-297-\thelp\n--\nnet/mac80211/Makefile=41=mac80211-$(CONFIG_MAC80211_LEDS) += led.o\nnet/mac80211/Makefile:42:mac80211-$(CONFIG_MAC80211_DEBUGFS) += \\\nnet/mac80211/Makefile-43-\tdebugfs.o \\\n--\nnet/mac80211/Makefile=60=rc80211_minstrel-y := \\\n--\nnet/mac80211/Makefile-62-\nnet/mac80211/Makefile:63:rc80211_minstrel-$(CONFIG_MAC80211_DEBUGFS) += \\\nnet/mac80211/Makefile-64-\trc80211_minstrel_ht_debugfs.o\n--\nnet/mac80211/debugfs.h-1-/* SPDX-License-Identifier: GPL-2.0 */\nnet/mac80211/debugfs.h:2:#ifndef __MAC80211_DEBUGFS_H\nnet/mac80211/debugfs.h:3:#define __MAC80211_DEBUGFS_H\nnet/mac80211/debugfs.h-4-\n--\nnet/mac80211/debugfs.h-6-\nnet/mac80211/debugfs.h:7:#ifdef CONFIG_MAC80211_DEBUGFS\nnet/mac80211/debugfs.h-8-void debugfs_hw_add(struct ieee80211_local *local);\n--\nnet/mac80211/debugfs.h=12=static inline void debugfs_hw_add(struct ieee80211_local *local)\n--\nnet/mac80211/debugfs.h-16-\nnet/mac80211/debugfs.h:17:#endif /* __MAC80211_DEBUGFS_H */\n--\nnet/mac80211/debugfs_key.h-1-/* SPDX-License-Identifier: GPL-2.0 */\nnet/mac80211/debugfs_key.h:2:#ifndef __MAC80211_DEBUGFS_KEY_H\nnet/mac80211/debugfs_key.h:3:#define __MAC80211_DEBUGFS_KEY_H\nnet/mac80211/debugfs_key.h-4-\nnet/mac80211/debugfs_key.h:5:#ifdef CONFIG_MAC80211_DEBUGFS\nnet/mac80211/debugfs_key.h-6-void ieee80211_debugfs_key_add(struct ieee80211_key *key);\n--\nnet/mac80211/debugfs_key.h=24=static inline void ieee80211_debugfs_key_remove_beacon_default(\n--\nnet/mac80211/debugfs_key.h-28-\nnet/mac80211/debugfs_key.h:29:#endif /* __MAC80211_DEBUGFS_KEY_H */\n--\nnet/mac80211/debugfs_netdev.h-12-\nnet/mac80211/debugfs_netdev.h:13:#ifdef CONFIG_MAC80211_DEBUGFS\nnet/mac80211/debugfs_netdev.h-14-void ieee80211_debugfs_remove_netdev(struct ieee80211_sub_if_data *sdata);\n--\nnet/mac80211/debugfs_sta.h-1-/* SPDX-License-Identifier: GPL-2.0 */\nnet/mac80211/debugfs_sta.h:2:#ifndef __MAC80211_DEBUGFS_STA_H\nnet/mac80211/debugfs_sta.h:3:#define __MAC80211_DEBUGFS_STA_H\nnet/mac80211/debugfs_sta.h-4-\n--\nnet/mac80211/debugfs_sta.h-6-\nnet/mac80211/debugfs_sta.h:7:#ifdef CONFIG_MAC80211_DEBUGFS\nnet/mac80211/debugfs_sta.h-8-void ieee80211_sta_debugfs_add(struct sta_info *sta);\n--\nnet/mac80211/debugfs_sta.h=24=static inline void ieee80211_link_sta_debugfs_drv_remove(struct link_sta_info *link_sta) {}\n--\nnet/mac80211/debugfs_sta.h-26-\nnet/mac80211/debugfs_sta.h:27:#endif /* __MAC80211_DEBUGFS_STA_H */\n--\nnet/mac80211/driver-ops.h=477=static inline void drv_sta_remove(struct ieee80211_local *local,\n--\nnet/mac80211/driver-ops.h-494-\nnet/mac80211/driver-ops.h:495:#ifdef CONFIG_MAC80211_DEBUGFS\nnet/mac80211/driver-ops.h-496-static inline void drv_vif_add_debugfs(struct ieee80211_local *local,\n--\nnet/mac80211/ieee80211_i.h=1112=struct ieee80211_link_data {\n--\nnet/mac80211/ieee80211_i.h-1164-\nnet/mac80211/ieee80211_i.h:1165:#ifdef CONFIG_MAC80211_DEBUGFS\nnet/mac80211/ieee80211_i.h-1166-\tstruct dentry *debugfs_dir;\n--\nnet/mac80211/ieee80211_i.h=1170=struct ieee80211_sub_if_data {\n--\nnet/mac80211/ieee80211_i.h-1257-\nnet/mac80211/ieee80211_i.h:1258:#ifdef CONFIG_MAC80211_DEBUGFS\nnet/mac80211/ieee80211_i.h-1259-\tstruct {\n--\nnet/mac80211/ieee80211_i.h=1461=struct ieee80211_local {\n--\nnet/mac80211/ieee80211_i.h-1711-\nnet/mac80211/ieee80211_i.h:1712:#ifdef CONFIG_MAC80211_DEBUGFS\nnet/mac80211/ieee80211_i.h-1713-\tstruct local_debugfsdentries {\n--\nnet/mac80211/key.h=59=struct ieee80211_key {\n--\nnet/mac80211/key.h-122-\nnet/mac80211/key.h:123:#ifdef CONFIG_MAC80211_DEBUGFS\nnet/mac80211/key.h-124-\tstruct {\n--\nnet/mac80211/rate.c=215=ieee80211_rate_control_ops_get(const char *name)\n--\nnet/mac80211/rate.c-240-\nnet/mac80211/rate.c:241:#ifdef CONFIG_MAC80211_DEBUGFS\nnet/mac80211/rate.c-242-static ssize_t rcname_read(struct file *file, char __user *userbuf,\n--\nnet/mac80211/rate.c=280=static void rate_control_free(struct ieee80211_local *local,\n--\nnet/mac80211/rate.c-284-\nnet/mac80211/rate.c:285:#ifdef CONFIG_MAC80211_DEBUGFS\nnet/mac80211/rate.c-286-\tdebugfs_remove_recursive(local-\u003edebugfs.rcdir);\n--\nnet/mac80211/rate.h=55=static inline void rate_control_add_sta_debugfs(struct sta_info *sta)\nnet/mac80211/rate.h-56-{\nnet/mac80211/rate.h:57:#ifdef CONFIG_MAC80211_DEBUGFS\nnet/mac80211/rate.h-58-\tstruct rate_control_ref *ref = sta-\u003erate_ctrl;\n--\nnet/mac80211/rate.h=67=static inline void rate_control_add_debugfs(struct ieee80211_local *local)\nnet/mac80211/rate.h-68-{\nnet/mac80211/rate.h:69:#ifdef CONFIG_MAC80211_DEBUGFS\nnet/mac80211/rate.h-70-\tstruct dentry *debugfsdir;\n--\nnet/mac80211/rc80211_minstrel_ht.c=1059=minstrel_ht_update_stats(struct minstrel_priv *mp, struct minstrel_ht_sta *mi)\n--\nnet/mac80211/rc80211_minstrel_ht.c-1181-\nnet/mac80211/rc80211_minstrel_ht.c:1182:#ifdef CONFIG_MAC80211_DEBUGFS\nnet/mac80211/rc80211_minstrel_ht.c-1183-\t/* use fixed index if set */\n--\nnet/mac80211/rc80211_minstrel_ht.c=1595=minstrel_ht_get_rate(void *priv, struct ieee80211_sta *sta, void *priv_sta,\n--\nnet/mac80211/rc80211_minstrel_ht.c-1606-\nnet/mac80211/rc80211_minstrel_ht.c:1607:#ifdef CONFIG_MAC80211_DEBUGFS\nnet/mac80211/rc80211_minstrel_ht.c-1608-\tif (mp-\u003efixed_rate_idx != -1)\n--\nnet/mac80211/rc80211_minstrel_ht.c=1915=minstrel_ht_alloc(struct ieee80211_hw *hw)\n--\nnet/mac80211/rc80211_minstrel_ht.c-1948-\nnet/mac80211/rc80211_minstrel_ht.c:1949:#ifdef CONFIG_MAC80211_DEBUGFS\nnet/mac80211/rc80211_minstrel_ht.c-1950-static void minstrel_ht_add_debugfs(struct ieee80211_hw *hw, void *priv,\n--\nnet/mac80211/rc80211_minstrel_ht.c=1983=static const struct rate_control_ops mac80211_minstrel_ht = {\n--\nnet/mac80211/rc80211_minstrel_ht.c-1993-\t.free = minstrel_ht_free,\nnet/mac80211/rc80211_minstrel_ht.c:1994:#ifdef CONFIG_MAC80211_DEBUGFS\nnet/mac80211/rc80211_minstrel_ht.c-1995-\t.add_debugfs = minstrel_ht_add_debugfs,\n--\nnet/mac80211/rc80211_minstrel_ht.h=75=struct minstrel_priv {\n--\nnet/mac80211/rc80211_minstrel_ht.h-85-\nnet/mac80211/rc80211_minstrel_ht.h:86:#ifdef CONFIG_MAC80211_DEBUGFS\nnet/mac80211/rc80211_minstrel_ht.h-87-\t/*\n--\nnet/mac80211/sta_info.h=526=struct link_sta_info {\n--\nnet/mac80211/sta_info.h-577-\nnet/mac80211/sta_info.h:578:#ifdef CONFIG_MAC80211_DEBUGFS\nnet/mac80211/sta_info.h-579-\tstruct dentry *debugfs_dir;\n--\nnet/mac80211/sta_info.h=790=struct sta_info {\n--\nnet/mac80211/sta_info.h-851-\nnet/mac80211/sta_info.h:852:#ifdef CONFIG_MAC80211_DEBUGFS\nnet/mac80211/sta_info.h-853-\tstruct dentry *debugfs_dir;\n--\nnet/mac80211/tx.c=2716=static struct sk_buff *ieee80211_build_hdr(struct ieee80211_sub_if_data *sdata,\n--\nnet/mac80211/tx.c-2741-\nnet/mac80211/tx.c:2742:#ifdef CONFIG_MAC80211_DEBUGFS\nnet/mac80211/tx.c-2743-\tif (local-\u003eforce_tx_status)\n--\nnet/mac80211/tx.c=3753=void __ieee80211_xmit_fast(struct ieee80211_sub_if_data *sdata,\n--\nnet/mac80211/tx.c-3799-\nnet/mac80211/tx.c:3800:#ifdef CONFIG_MAC80211_DEBUGFS\nnet/mac80211/tx.c-3801-\tif (local-\u003eforce_tx_status)\n"
} |
| 17/2 | 2026/09/24 13:42 | llm | patch-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 99190 output: 38 thoughts: 108 Thoughts: **Investigating DebugFS Exposure in mac80211** Okay, let's dive in and see how `mac80211` exposes its internals through DebugFS. First things first, I need to check the `Kconfig` file in `net/mac80211`, specifically focusing on the `MAC80211_DEBUGFS` configuration option. Let me get that line number... yes, line 88. Alright, here it is: `config MAC80211_DEBUGFS bool "Export mac80211 internals in DebugFS" depends on MAC80211 && DEBUG_FS default y`. Good, it's a boolean, and as expected, it depends on both `MAC80211` and `DEBUG_FS` being enabled. Seems straightforward. It defaults to "y", which is what I'd expect for debug features. Just to be thorough, I should scan a few lines around that. Let's see... lines 85 to 95, just to get a good contextual understanding of what else might be related. Right, let's take a look. |
| 18/2 | 2026/09/24 13:42 | tool | read-file |
0mArgs:{
"File": "net/mac80211/Kconfig",
"FirstLine": 85,
"LineCount": 20
}Results: {
"Contents": " 85:\t\t This option enables a few LED triggers for different\n 86:\t\t packet receive/transmit events.\n 87:\t\n 88:\tconfig MAC80211_DEBUGFS\n 89:\t\tbool \"Export mac80211 internals in DebugFS\"\n 90:\t\tdepends on MAC80211 \u0026\u0026 CFG80211_DEBUGFS\n 91:\t\thelp\n 92:\t\t Select this to see extensive information about\n 93:\t\t the internal state of mac80211 in debugfs.\n 94:\t\n 95:\t\t Say N unless you know you need this.\n 96:\t\n 97:\tconfig MAC80211_MESSAGE_TRACING\n 98:\t\tbool \"Trace all mac80211 debug messages\"\n 99:\t\tdepends on MAC80211 \u0026\u0026 TRACING\n 100:\t\thelp\n 101:\t\t Select this option to have mac80211 register the\n 102:\t\t mac80211_msg trace subsystem with tracepoints to\n 103:\t\t collect all debugging messages, independent of\n 104:\t\t printing them into the kernel log.\n"
} |
| 19/2 | 2026/09/24 13:42 | llm | patch-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 99895 output: 32 thoughts: 73 Thoughts: Here's my summary, as I delve into this: **Digging into the MAC80211 DebugFS Dependency** Okay, so `MAC80211_DEBUGFS` is clearly reliant on `MAC80211` and `CFG80211_DEBUGFS`. That's straightforward enough. Now, the crucial question is, what other configurations does `CFG80211_DEBUGFS` depend on, if any? I need to understand the complete dependency tree to ensure this is enabled appropriately for my use-case. Time to navigate over to `net/wireless/Kconfig` and take a look at the configuration options related to `CFG80211_DEBUGFS`. That should give me the full picture. |
| 20/2 | 2026/09/24 13:42 | tool | grepper |
0mArgs:{
"Expression": "CFG80211_DEBUGFS",
"PathPrefix": "net/wireless"
}Results: {
"Output": "net/wireless/Kconfig=159=config CFG80211_DEFAULT_PS\n--\nnet/wireless/Kconfig-168-\nnet/wireless/Kconfig:169:config CFG80211_DEBUGFS\nnet/wireless/Kconfig-170-\tbool \"cfg80211 DebugFS entries\"\n--\nnet/wireless/Makefile=12=cfg80211-$(CONFIG_OF) += of.o\nnet/wireless/Makefile:13:cfg80211-$(CONFIG_CFG80211_DEBUGFS) += debugfs.o\nnet/wireless/Makefile-14-cfg80211-$(CONFIG_CFG80211_WEXT) += wext-compat.o wext-sme.o\n--\nnet/wireless/debugfs.h-1-/* SPDX-License-Identifier: GPL-2.0 */\nnet/wireless/debugfs.h:2:#ifndef __CFG80211_DEBUGFS_H\nnet/wireless/debugfs.h:3:#define __CFG80211_DEBUGFS_H\nnet/wireless/debugfs.h-4-\nnet/wireless/debugfs.h:5:#ifdef CONFIG_CFG80211_DEBUGFS\nnet/wireless/debugfs.h-6-void cfg80211_debugfs_rdev_add(struct cfg80211_registered_device *rdev);\n--\nnet/wireless/debugfs.h=9=void cfg80211_debugfs_rdev_add(struct cfg80211_registered_device *rdev) {}\n--\nnet/wireless/debugfs.h-11-\nnet/wireless/debugfs.h:12:#endif /* __CFG80211_DEBUGFS_H */\n"
} |
| 21/2 | 2026/09/24 13:42 | llm | patch-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 100613 output: 34 thoughts: 22 Thoughts: Okay, let's dive into this. Here's my take on those lines: **Analyzing `net/wireless/Kconfig`** Alright, time to get my hands dirty. I need to understand this configuration. I'm going to jump straight into `net/wireless/Kconfig` and focus on lines 168-175. That specific block is likely key to understanding something specific in the wireless configuration. I'm hoping to quickly grasp the dependencies, options, or potentially the introduction or modification of a particular feature set. I need to quickly identify the feature itself, and then understand how it fits into the broader wireless landscape of the kernel. What are its implications, and how does it relate to other existing or potentially conflicting options? A focused look at those lines should give me the core idea, and then I can broaden my scope if needed to trace any dependencies or understand context, if I need it. |
| 22/2 | 2026/09/24 13:43 | tool | read-file |
0mArgs:{
"File": "net/wireless/Kconfig",
"FirstLine": 168,
"LineCount": 10
}Results: {
"Contents": " 168:\t\n 169:\tconfig CFG80211_DEBUGFS\n 170:\t\tbool \"cfg80211 DebugFS entries\"\n 171:\t\tdepends on DEBUG_FS\n 172:\t\thelp\n 173:\t\t You can enable this if you want debugfs entries for cfg80211.\n 174:\t\n 175:\t\t If unsure, say N.\n 176:\t\n 177:\tconfig CFG80211_CRDA_SUPPORT\n"
} |
| 23/2 | 2026/09/24 13:43 | llm | patch-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 101012 output: 35 thoughts: 764 Thoughts: **Identifying Relevant Configurations for Fuzzing** Okay, here's the breakdown of my thinking. My goal is to determine the optimal configuration options to enable for effective fuzzing, specifically focusing on the wireless subsystem. First, I need to identify the key modules and their dependencies. We know we're dealing with `CFG80211`, `MAC80211`, `MAC80211_HWSIM`, and related debug interfaces (`CFG80211_DEBUGFS`, `MAC80211_DEBUGFS`). I'll need to confirm that `scs.o` is always built into `cfg80211` and `mac80211` when those configs are enabled. Then I'll check the Makefile for `mac80211_hwsim` to verify that `mac80211_hwsim.o` is built with `CONFIG_MAC80211_HWSIM`. Next, I considered `CONFIG_MT7996E` as a possible configuration. This seems relevant, since the patch touches that driver's source code, but I quickly realized that the MT7996 is a hardware-specific PCIe card. It's not reachable in a purely virtual environment like the one syzkaller runs in with `mac80211_hwsim`. So I'm going to set it aside for now. That said, it's worth noting to potentially include `MT7996E` in `EnableConfigs` if someone actually compiles it, but it's not critical for this fuzzing effort. I went back to the instructions regarding identifying `EnableConfigs`. They emphasize a methodical approach: inspecting Kconfig files and `#ifdef` guards, and paying attention to "depends on" lines to include parent subsystem configs. This reinforces the need to be thorough and not make assumptions. Given this, I have a clear set of configurations to prioritize: - `CFG80211` - `MAC80211` - `MAC80211_HWSIM` - `CFG80211_DEBUGFS` - `MAC80211_DEBUGFS` I need to confirm if there are any other configuration guards within the source files themselves, so I will now carefully check for `#ifdef` or `IS_ENABLED` in `net/mac80211/scs.c`, `net/wireless/scs.c`, and `net/wireless/nl80211.c` to be absolutely certain I'm not missing anything. |
| 24/2 | 2026/09/24 13:43 | tool | grepper |
0mError:no matches Args: {
"Expression": "(#if|IS_ENABLED)",
"PathPrefix": "net/wireless/scs.c"
}Results: {
"error": "no matches"
} |
| 25/2 | 2026/09/24 13:43 | llm | patch-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 102391 output: 40 thoughts: 29 Thoughts: Okay, here's my take on those thoughts, tailored for an expert audience: **Diving into `net/mac80211/scs.c` for Conditional Compilation** Alright, my immediate thought is to zero in on `net/mac80211/scs.c`. Given the context – whatever we're troubleshooting or improving – my first line of investigation will undoubtedly be to scan that file for any `#if` preprocessor directives or uses of `IS_ENABLED()`. The goal, of course, is to understand and potentially influence any conditional compilation paths related to this section of the mac80211 stack. We all know how easily complexity creeps in with feature flags, and a quick check of those directives is usually the best place to start when trying to understand what can influence any observed behavior. I'm expecting to find features that can be optionally included or excluded and the conditions under which that happens, and it may give some clues on the next logical steps for troubleshooting. I'm especially interested in figuring out what dependencies or configuration options trigger the inclusion of code paths that are currently active. |
| 26/2 | 2026/09/24 13:43 | tool | grepper |
0mError:no matches Args: {
"Expression": "(#if|IS_ENABLED)",
"PathPrefix": "net/mac80211/scs.c"
}Results: {
"error": "no matches"
} |
| 27/2 | 2026/09/24 13:43 | llm | patch-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 102710 output: 447 thoughts: 2904 Thoughts: **Fuzzing Candidate Assessment: SCS/MSCS Implementation** Okay, so I've been diving into this Stream Classification Service (SCS) and Mirrored SCS (MSCS) implementation, and I need to determine if it's a good candidate for fuzzing. First, I noticed that the core logic isn't conditionally compiled with the typical `#if` or `IS_ENABLED` macros, which immediately suggests it's potentially always present, or at least enabled via a configuration option. I traced the `scs` and `mscs` debugfs entries to `net/mac80211/debugfs_sta.c`, and confirmed they are tied to `CONFIG_MAC80211_DEBUGFS`. That's one enabling config. Now, let's nail down the critical functions. I went through the relevant files to identify the functions that are either added or substantially modified for SCS/MSCS functionality. The important symbols are: `nl80211_set_scs`, `nl80211_set_mscs`, `nl80211_parse_scs_desc`, `ieee80211_set_scs`, `ieee80211_set_mscs`, `ieee80211_flow_classify`, `ieee80211_flow_learn`, `cfg80211_parse_tclas`, `cfg80211_parse_tclas_mask`, `cfg80211_flow_parse`, `cfg80211_flow_key_build`, `cfg80211_scs_evaluate`, and `mac80211_hwsim_sta_set_scs`. These handle the netlink interface for configuring SCS and MSCS (via `nl80211_set_scs` and `nl80211_set_mscs`), the core classification logic (`ieee80211_flow_classify`, `ieee80211_flow_learn`), and the underlying configuration parsing and evaluation. Specifically: - `nl80211_set_scs` / `nl80211_set_mscs` provide the netlink interface. - `ieee80211_set_scs` / `ieee80211_set_mscs` set the SCS configuration. - `ieee80211_flow_classify` is called from the transmit path. - `ieee80211_flow_learn` is called from the receive path. - `cfg80211_parse_tclas`, `cfg80211_parse_tclas_mask`, `cfg80211_flow_parse`, `cfg80211_flow_key_build`, and `cfg80211_scs_evaluate` are used for parsing and matching. - `mac80211_hwsim_sta_set_scs` sets the configuration for the virtual hardware simulation. I double-checked these and they are implemented in relevant SCS code. It is clear that these are key entry points and implementations. Now, I took a closer look at `nl80211_set_scs`. It deals with netlink parsing, memory allocation (`desc`, `res`), iterative processing of nested attributes, descriptor swapping, and calls `ieee80211_set_scs`. `ieee80211_set_scs` does more memory management: allocation with `kvzalloc`, `memmove` operations, RCU pointer manipulation, and deallocation with `kvfree_rcu`. `ieee80211_flow_classify` has the critical path RCU dereferencing and parsing with `cfg80211_flow_parse`. Finally, `ieee80211_flow_learn` handles flow insertion using `kzalloc_obj`, rhashtable insertion, and a delayed work queue, along with garbage collection work. This combination, I can see, is a prime target! We're talking netlink interaction, dynamic memory management, RCU, rhashtable lookups/inserts, packet classification in fast paths, all of this increases the likelihood of finding memory corruption bugs. The interaction with packet parsing and classification is particularly concerning. Regarding dependencies, the core configurations are `CFG80211`, `MAC80211`, `MAC80211_HWSIM`, `CFG80211_DEBUGFS`, `MAC802 |
| 28/2 | 2026/09/24 13:43 | tool | set-results |
0mArgs:{
"EnableConfigs": [
"CFG80211",
"MAC80211",
"MAC80211_HWSIM",
"CFG80211_DEBUGFS",
"MAC80211_DEBUGFS",
"DEBUG_FS"
],
"FocusSymbols": [
"nl80211_set_scs",
"nl80211_set_mscs",
"nl80211_parse_scs_desc",
"ieee80211_set_scs",
"ieee80211_set_mscs",
"ieee80211_flow_classify",
"ieee80211_flow_learn",
"cfg80211_parse_tclas",
"cfg80211_parse_tclas_mask",
"cfg80211_flow_parse",
"cfg80211_flow_key_build",
"cfg80211_scs_evaluate",
"mac80211_hwsim_sta_set_scs"
],
"Reasoning": "The patch series implements support for Stream Classification Service (SCS) and Mirrored SCS (MSCS) across cfg80211, mac80211, and the virtual mac80211_hwsim driver. It introduces new user-accessible netlink commands (NL80211_CMD_SET_SCS and NL80211_CMD_SET_MSCS), TCLAS element parsing, flow key matching, rhashtable management, and packet classification in RX/TX fast paths. All of this code is directly reachable in standard virtualized environments (e.g. QEMU) using mac80211_hwsim and nl80211 netlink sockets, making it highly valuable for fuzzing.",
"WorthFuzzing": true
}Results: {
"EnableConfigs": [
"CFG80211",
"MAC80211",
"MAC80211_HWSIM",
"CFG80211_DEBUGFS",
"MAC80211_DEBUGFS",
"DEBUG_FS"
],
"FocusSymbols": [
"nl80211_set_scs",
"nl80211_set_mscs",
"nl80211_parse_scs_desc",
"ieee80211_set_scs",
"ieee80211_set_mscs",
"ieee80211_flow_classify",
"ieee80211_flow_learn",
"cfg80211_parse_tclas",
"cfg80211_parse_tclas_mask",
"cfg80211_flow_parse",
"cfg80211_flow_key_build",
"cfg80211_scs_evaluate",
"mac80211_hwsim_sta_set_scs"
],
"Reasoning": "The patch series implements support for Stream Classification Service (SCS) and Mirrored SCS (MSCS) across cfg80211, mac80211, and the virtual mac80211_hwsim driver. It introduces new user-accessible netlink commands (NL80211_CMD_SET_SCS and NL80211_CMD_SET_MSCS), TCLAS element parsing, flow key matching, rhashtable management, and packet classification in RX/TX fast paths. All of this code is directly reachable in standard virtualized environments (e.g. QEMU) using mac80211_hwsim and nl80211 netlink sockets, making it highly valuable for fuzzing.",
"WorthFuzzing": true
} |
| 29/1 | 2026/09/24 13:43 | action | If |
2mArgs:{
"WorthFuzzing": true
}Results: null |
| 30/2 | 2026/09/24 13:43 | agent | kmsan-evaluator |
2mModel:core Args: null Results: {
"KMSANReasoning": "The patch series implements Stream Classification Service (SCS) and Mirrored SCS (MSCS) support across cfg80211, mac80211, and the mt7996 / mac80211_hwsim drivers.\n\nKMSAN vs KASAN Applicability Analysis:\n1. User-space copies / Info-leaks: Responses to netlink commands (NL80211_CMD_SET_SCS and NL80211_CMD_SET_MSCS) send discrete scalar values using nla_put_u8 and nla_put_u16 rather than writing entire structures or aggregates containing padding bytes. There are no kernel-to-user info-leak risks.\n2. Structure allocations and initializations: All newly introduced heap allocations (such as cfg80211_scs_desc, ieee80211_scs_sta, ieee80211_mscs_sta, and ieee80211_flow_entry) are allocated with kzalloc, kvzalloc, kcalloc, or kzalloc_obj. Stack structures (e.g., cfg80211_flow_info, cfg80211_flow_key, cfg80211_tclas, cfg80211_mscs_desc) are consistently zeroed either at declaration via '= {}' or through explicit memset() calls before parsing and manipulation.\n3. Flow key comparison and hashing: Structures compared with memcmp or hashed via rhashtable (cfg80211_flow_key and ieee80211_flow_hkey) have their layouts verified with static_assert against padding, and rhashtable key lengths strictly bound the lookup key length to offsetofend(..., key).\n4. Firmware communication: In mt7996_mcu_set_scs, the firmware request struct is initialized with designated initializers '= { ... }', ensuring all padding and reserved fields are zero-filled.\n5. Bug profile: The primary risk surface of the added element parsers, frame classifiers, and driver callbacks involves out-of-bounds array reads, parser boundary validation, concurrency/locking, or object lifetime management. These failure modes are comprehensively covered by KASAN, UBSAN, and LOCKDEP.\n\nBecause no uninitialized memory usage or info-leak vectors are introduced, a dedicated KMSAN fuzzing session is not justified.",
"NeedsKMSAN": false
}Instruction: You are an expert Linux kernel security engineer specializing in kernel memory
error detectors (KASAN and KMSAN). Your job is to review the provided patch series and
determine if the code changes justify spawning a dedicated KMSAN (KernelMemorySanitizer)
fuzzing session in addition to standard KASAN fuzzing.
CRITICAL DISTINCTION BETWEEN KASAN AND KMSAN:
- Standard KASAN kernel builds (upstream-apparmor-kasan.config) already enable
a comprehensive suite of debugging tools and sanitizers, including KASAN
(out-of-bounds accesses, use-after-free, double free, invalid free), LOCKDEP
(locking bugs and deadlocks), UB-sanitizers, and memory corruption checks.
- KMSAN (KernelMemorySanitizer) detects reads of UNINITIALIZED memory (stack, heap,
or page allocations) and kernel-to-user memory info-leaks.
Rule: THERE IS NO SENSE IN RUNNING A KMSAN SESSION IF A BUG CAN BE CAUGHT BY KASAN,
LOCKDEP, OR OTHER STANDARD BUG DETECTORS.
A dedicated KMSAN fuzzing session incurs significant resource costs. You must ONLY
set NeedsKMSAN=true if the code changes introduce or expose UNINITIALIZED MEMORY risks
that are detected ONLY by KMSAN.
Look holistically at the patch series and surrounding code. Even if no direct
uninitialized field accesses or new buffer allocations are added in the diff itself,
a patch may alter control flow, bounds checking, or data length calculations in ways
that change how the rest of the code operates on existing buffers (e.g. allowing
uninitialized stack/heap memory to be read, copied to user space, or used in control
flow). Do not hesitate to use your code access tools to inspect the surrounding code,
called functions, and callers.
Set NeedsKMSAN=true ONLY IF the patch introduces or modifies:
1. Kernel structures sent to user space (via copy_to_user, put_user, netlink skb
attributes, ioctl output arguments, socket options, or BPF buffers) where fields
or structure padding might not be fully initialized/zeroed.
2. Conditional logic or branching that depends on potentially uninitialized variables
or struct fields.
3. Allocation or initialization of complex data structures where uninitialized fields
could be read later in reachable code paths.
4. Bounds checks, lengths, or logic in a way that allows surrounding code to access
uninitialized bytes of existing buffers.
Set NeedsKMSAN=false IF:
- The code changes primarily risk out-of-bounds access, array overflows, NULL pointer
dereferences, locking deadlocks, or use-after-free bugs (these are already caught
by KASAN, LOCKDEP, or standard bug detectors).
- All stack/heap structures touched or introduced by the patch are fully zeroed
or initialized (e.g. using = {0}, memset, kzalloc) before being read or copied.
- The patch does not introduce any risk of uninitialized memory usage or info-leaks.
Use your code access tools to inspect the surrounding code if necessary, then provide
detailed KMSANReasoning contrasting KASAN vs KMSAN applicability for this patch.
Prefer calling several tools at the same time to save round-trips.
Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.
Prompt: Target architecture: amd64
For your convenience, here is the diff of the changes:
commit 80a1526dde5ea12c2da885d6b6b0e2c4c75ac2a4
Author: syz-cluster <triage@syzkaller.com>
Date: Thu Sep 24 13:41:14 2026 +0000
syz-cluster: applied patch under review
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/init.c b/drivers/net/wireless/mediatek/mt76/mt7996/init.c
index fb635a0925840..ca161d644d728 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/init.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/init.c
@@ -526,6 +526,8 @@ mt7996_init_wiphy(struct ieee80211_hw *hw, struct mtk_wed_device *wed)
wiphy_ext_feature_set(wiphy, NL80211_EXT_FEATURE_CAN_REPLACE_PTK0);
wiphy_ext_feature_set(wiphy, NL80211_EXT_FEATURE_MU_MIMO_AIR_SNIFFER);
wiphy_ext_feature_set(wiphy, NL80211_EXT_FEATURE_SET_SCAN_DWELL);
+ wiphy_ext_feature_set(wiphy, NL80211_EXT_FEATURE_SCS);
+ wiphy_ext_feature_set(wiphy, NL80211_EXT_FEATURE_MSCS);
if (mt7996_eeprom_has_background_radar(dev) &&
(!mdev->dev->of_node ||
@@ -1580,6 +1582,7 @@ mt7996_init_eht_caps(struct mt7996_phy *phy, enum nl80211_band band,
eht_cap_elem->mac_cap_info[0] =
IEEE80211_EHT_MAC_CAP0_OM_CONTROL |
+ IEEE80211_EHT_MAC_CAP0_SCS_TRAFFIC_DESC |
u8_encode_bits(IEEE80211_EHT_MAC_CAP0_MAX_MPDU_LEN_11454,
IEEE80211_EHT_MAC_CAP0_MAX_MPDU_LEN_MASK);
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index e218856b0c454..6b2d24fe70f10 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -2051,6 +2051,69 @@ static void mt7996_sta_set_4addr(struct ieee80211_hw *hw,
mutex_unlock(&dev->mt76.mutex);
}
+/* SCS descriptors belong to the MLD, firmware streams to a link */
+static int mt7996_sta_scs_program(struct mt7996_dev *dev,
+ struct ieee80211_vif *vif,
+ struct ieee80211_sta *sta,
+ const struct cfg80211_scs_desc *desc,
+ bool del)
+{
+ struct mt7996_sta *msta = (struct mt7996_sta *)sta->drv_priv;
+ struct ieee80211_link_sta *link_sta;
+ unsigned int link_id;
+ bool any = false;
+ int ret = 0;
+
+ for_each_sta_active_link(vif, sta, link_sta, link_id) {
+ struct mt7996_sta_link *msta_link;
+
+ msta_link = mt7996_sta_link_protected(dev, msta, link_id);
+ if (!msta_link || !msta_link->wcid.sta)
+ continue;
+
+ any = true;
+ if (mt7996_mcu_set_scs(dev, msta_link->wcid.idx, desc, del))
+ ret = -EIO;
+ }
+
+ return any ? ret : -EIO;
+}
+
+static int mt7996_sta_set_scs(struct ieee80211_hw *hw,
+ struct ieee80211_vif *vif,
+ struct ieee80211_sta *sta,
+ struct cfg80211_scs_desc * const *desc,
+ struct cfg80211_scs_result *res, u8 n_desc)
+{
+ struct mt7996_dev *dev = mt7996_hw_dev(hw);
+ u8 i;
+
+ mutex_lock(&dev->mt76.mutex);
+
+ for (i = 0; i < n_desc; i++) {
+ const struct cfg80211_scs_desc *d = desc[i];
+ bool del = d->req_type == NL80211_SCS_REQ_REMOVE ||
+ !d->qos_char;
+
+ /* The firmware only supports uplink streams */
+ if (!del && ieee80211_qos_char_direction(d->qos_char) !=
+ IEEE80211_QOS_CHAR_DIR_UPLINK) {
+ res[i].status = WLAN_STATUS_REQUEST_DECLINED;
+ continue;
+ }
+
+ if (!mt7996_sta_scs_program(dev, vif, sta, d, del) || del)
+ continue;
+
+ mt7996_sta_scs_program(dev, vif, sta, d, true);
+ res[i].status = WLAN_STATUS_REQUEST_DECLINED;
+ }
+
+ mutex_unlock(&dev->mt76.mutex);
+
+ return 0;
+}
+
static void mt7996_sta_set_decap_offload(struct ieee80211_hw *hw,
struct ieee80211_vif *vif,
struct ieee80211_sta *sta,
@@ -2588,6 +2651,7 @@ const struct ieee80211_ops mt7996_ops = {
.set_coverage_class = mt7996_set_coverage_class,
.sta_statistics = mt7996_sta_statistics,
.sta_set_4addr = mt7996_sta_set_4addr,
+ .sta_set_scs = mt7996_sta_set_scs,
.sta_set_decap_offload = mt7996_sta_set_decap_offload,
.add_twt_setup = mt7996_mac_add_twt_setup,
.twt_teardown_request = mt7996_twt_teardown_request,
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
index cd7587ce5b8a6..fceffdeb8ca50 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
@@ -2616,6 +2616,45 @@ int mt7996_mcu_add_rate_ctrl(struct mt7996_dev *dev, struct mt7996_sta *msta,
return ret;
}
+int mt7996_mcu_set_scs(struct mt7996_dev *dev, u16 wlan_idx,
+ const struct cfg80211_scs_desc *desc, bool del)
+{
+#define MURU_QOS_FLAG_UPDATE 20
+#define MURU_QOS_FLAG_DELETE 21
+ struct {
+ u8 __rsv1[4];
+
+ __le16 tag;
+ __le16 len;
+ __le32 qos_flag;
+ __le16 wlan_idx;
+ u8 __rsv2[12];
+ u8 dir;
+ u8 __rsv3[4];
+ u8 scs_id;
+ u8 qos_ie[44];
+ } __packed req = {
+ .tag = cpu_to_le16(UNI_CMD_MURU_SET_QOS_CFG),
+ .len = cpu_to_le16(sizeof(req) - 4),
+ .wlan_idx = cpu_to_le16(wlan_idx),
+ .scs_id = desc->id,
+ };
+
+ if (del) {
+ req.qos_flag = cpu_to_le32(MURU_QOS_FLAG_DELETE);
+ } else {
+ if (desc->qos_char_len > sizeof(req.qos_ie))
+ return -EINVAL;
+
+ req.qos_flag = cpu_to_le32(MURU_QOS_FLAG_UPDATE);
+ req.dir = ieee80211_qos_char_direction(desc->qos_char);
+ memcpy(req.qos_ie, desc->qos_char, desc->qos_char_len);
+ }
+
+ return mt76_mcu_send_msg(&dev->mt76, MCU_WMWA_UNI_CMD(MURU), &req,
+ sizeof(req), true);
+}
+
static int
mt7996_mcu_add_group(struct mt7996_dev *dev, struct mt7996_vif_link *link,
struct mt76_wcid *wcid)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.h b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.h
index 74b70fb6da3da..efd474cf8da9c 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.h
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.h
@@ -990,6 +990,10 @@ enum {
UNI_EXT_EEPROM_ACCESS = 1,
};
+enum {
+ UNI_CMD_MURU_SET_QOS_CFG = 0xfe,
+};
+
enum {
UNI_VOW_DRR_CTRL,
UNI_VOW_RX_AT_AIRTIME_EN = 0x0b,
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h b/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
index 2a0cdb56f822e..ebd55d8da654f 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
@@ -754,6 +754,8 @@ int mt7996_mcu_set_fixed_rate_ctrl(struct mt7996_dev *dev,
void *data, u16 version);
int mt7996_mcu_set_fixed_field(struct mt7996_dev *dev, struct mt7996_sta *msta,
void *data, u8 link_id, u32 field);
+int mt7996_mcu_set_scs(struct mt7996_dev *dev, u16 wlan_idx,
+ const struct cfg80211_scs_desc *desc, bool del);
int mt7996_mcu_set_eeprom(struct mt7996_dev *dev);
int mt7996_mcu_get_eeprom(struct mt7996_dev *dev, u32 offset, u8 *buf, u32 buf_len,
enum mt7996_eeprom_mode mode);
diff --git a/drivers/net/wireless/virtual/mac80211_hwsim_i.h b/drivers/net/wireless/virtual/mac80211_hwsim_i.h
index 0f0f2ac6d80eb..7f1c85ea6813e 100644
--- a/drivers/net/wireless/virtual/mac80211_hwsim_i.h
+++ b/drivers/net/wireless/virtual/mac80211_hwsim_i.h
@@ -137,6 +137,9 @@ struct mac80211_hwsim_data {
/* RSSI in rx status of the receiver */
int rx_rssi;
+ /* Status code for a traffic description, 0 accepts */
+ u16 scs_status;
+
/* only used when pmsr capability is supplied */
struct cfg80211_pmsr_capabilities pmsr_capa;
struct cfg80211_pmsr_request *pmsr_request;
diff --git a/drivers/net/wireless/virtual/mac80211_hwsim_main.c b/drivers/net/wireless/virtual/mac80211_hwsim_main.c
index d567ed8d45a43..828dde53747a5 100644
--- a/drivers/net/wireless/virtual/mac80211_hwsim_main.c
+++ b/drivers/net/wireless/virtual/mac80211_hwsim_main.c
@@ -4294,6 +4294,24 @@ static int mac80211_hwsim_set_radar_background(struct ieee80211_hw *hw,
return 0;
}
+/* The status code for a traffic description is set via debugfs for testing */
+static int mac80211_hwsim_sta_set_scs(struct ieee80211_hw *hw,
+ struct ieee80211_vif *vif,
+ struct ieee80211_sta *sta,
+ struct cfg80211_scs_desc * const *desc,
+ struct cfg80211_scs_result *res,
+ u8 n_desc)
+{
+ struct mac80211_hwsim_data *data = hw->priv;
+ u8 i;
+
+ for (i = 0; i < n_desc; i++)
+ if (desc[i]->qos_char)
+ res[i].status = data->scs_status;
+
+ return 0;
+}
+
#ifdef CONFIG_MAC80211_DEBUGFS
#define HWSIM_DEBUGFS_OPS \
.link_add_debugfs = mac80211_hwsim_link_add_debugfs,
@@ -4318,6 +4336,7 @@ static int mac80211_hwsim_set_radar_background(struct ieee80211_hw *hw,
.link_sta_rc_update = mac80211_hwsim_sta_rc_update, \
.conf_tx = mac80211_hwsim_conf_tx, \
.get_survey = mac80211_hwsim_get_survey, \
+ .sta_set_scs = mac80211_hwsim_sta_set_scs, \
CFG80211_TESTMODE_CMD(mac80211_hwsim_testmode_cmd) \
.ampdu_action = mac80211_hwsim_ampdu_action, \
.flush = mac80211_hwsim_flush, \
@@ -5973,6 +5992,8 @@ static int mac80211_hwsim_new_radio(struct genl_info *info,
NL80211_EXT_FEATURE_EXT_KEY_ID);
wiphy_ext_feature_set(hw->wiphy,
NL80211_EXT_FEATURE_ASSOC_FRAME_ENCRYPTION);
+ wiphy_ext_feature_set(hw->wiphy, NL80211_EXT_FEATURE_SCS);
+ wiphy_ext_feature_set(hw->wiphy, NL80211_EXT_FEATURE_MSCS);
hw->wiphy->interface_modes = param->iftypes;
@@ -6162,6 +6183,8 @@ static int mac80211_hwsim_new_radio(struct genl_info *info,
&hwsim_fops_group);
debugfs_create_file("rx_rssi", 0666, data->debugfs, data,
&hwsim_fops_rx_rssi);
+ debugfs_create_u16("scs_status", 0600, data->debugfs,
+ &data->scs_status);
if (!data->use_chanctx)
debugfs_create_file("dfs_simulate_radar", 0222,
data->debugfs,
diff --git a/include/linux/ieee80211-eht.h b/include/linux/ieee80211-eht.h
index a699d108eee61..92fa7efdf156c 100644
--- a/include/linux/ieee80211-eht.h
+++ b/include/linux/ieee80211-eht.h
@@ -1385,4 +1385,129 @@ static inline u32 ieee80211_eml_trans_timeout_in_us(u16 eml_cap)
_data + ieee80211_mle_common_size(_data),\
_len - ieee80211_mle_common_size(_data))
+/**
+ * struct ieee80211_qos_char_elem - QoS Characteristics element
+ * @element_id: %WLAN_EID_EXTENSION
+ * @length: length of everything behind it
+ * @element_id_ext: %WLAN_EID_EXT_QOS_CHARACTERISTICS
+ * @control: control info, see %IEEE80211_QOS_CHAR_CTRL_*
+ * @min_service_interval: minimum service period interval, in microseconds
+ * @max_service_interval: maximum service period interval, in microseconds
+ * @min_data_rate: minimum data rate at the MAC SAP, in kilobits per second
+ * @delay_bound: targeted transport time of an MSDU, in microseconds
+ * @variable: the optional parameters that the presence bitmap names, in the
+ * order of the %IEEE80211_QOS_CHAR_PRES_* bits
+ *
+ * The whole element, header included, because a device takes it unchanged.
+ */
+struct ieee80211_qos_char_elem {
+ u8 element_id;
+ u8 length;
+ u8 element_id_ext;
+ __le32 control;
+ __le32 min_service_interval;
+ __le32 max_service_interval;
+ u8 min_data_rate[3];
+ u8 delay_bound[3];
+ u8 variable[];
+} __packed;
+
+#define IEEE80211_QOS_CHAR_CTRL_DIRECTION GENMASK(1, 0)
+#define IEEE80211_QOS_CHAR_CTRL_PRESENCE GENMASK(24, 9)
+
+#define IEEE80211_QOS_CHAR_DIR_UPLINK 0
+#define IEEE80211_QOS_CHAR_DIR_DOWNLINK 1
+#define IEEE80211_QOS_CHAR_DIR_DIRECT 2
+
+#define IEEE80211_QOS_CHAR_PRES_MAX_MSDU_SIZE BIT(0)
+#define IEEE80211_QOS_CHAR_PRES_SERVICE_START_TIME BIT(1)
+#define IEEE80211_QOS_CHAR_PRES_SERVICE_START_LINK_ID BIT(2)
+#define IEEE80211_QOS_CHAR_PRES_MEAN_DATA_RATE BIT(3)
+#define IEEE80211_QOS_CHAR_PRES_BURST_SIZE BIT(4)
+#define IEEE80211_QOS_CHAR_PRES_MSDU_LIFETIME BIT(5)
+#define IEEE80211_QOS_CHAR_PRES_MSDU_DELIVERY_INFO BIT(6)
+#define IEEE80211_QOS_CHAR_PRES_MEDIUM_TIME BIT(7)
+
+/**
+ * ieee80211_qos_char_presence - QoS Characteristics presence bitmap
+ * @qc: the element
+ * Return: the Presence Bitmap Of Additional Parameters subfield, a bitmap of
+ * %IEEE80211_QOS_CHAR_PRES_*
+ */
+static inline u16
+ieee80211_qos_char_presence(const struct ieee80211_qos_char_elem *qc)
+{
+ return u32_get_bits(le32_to_cpu(qc->control),
+ IEEE80211_QOS_CHAR_CTRL_PRESENCE);
+}
+
+/**
+ * ieee80211_qos_char_direction - QoS Characteristics direction
+ * @qc: the element
+ * Return: %IEEE80211_QOS_CHAR_DIR_UPLINK, _DOWNLINK or _DIRECT
+ */
+static inline u8
+ieee80211_qos_char_direction(const struct ieee80211_qos_char_elem *qc)
+{
+ return u32_get_bits(le32_to_cpu(qc->control),
+ IEEE80211_QOS_CHAR_CTRL_DIRECTION);
+}
+
+/**
+ * ieee80211_qos_char_size_ok - check a QoS Characteristics element
+ * @data: candidate octets, from the Element ID
+ * @len: length of @data
+ *
+ * Call this before the other accessors, which assume a valid element.
+ *
+ * Return: %true if @data holds a QoS Characteristics element whose length
+ * matches its presence bitmap
+ */
+static inline bool ieee80211_qos_char_size_ok(const u8 *data, size_t len)
+{
+ const struct ieee80211_qos_char_elem *qc = (const void *)data;
+ size_t needed = sizeof(*qc);
+ u16 present;
+
+ if (len < needed)
+ return false;
+
+ if (qc->element_id != WLAN_EID_EXTENSION ||
+ qc->element_id_ext != WLAN_EID_EXT_QOS_CHARACTERISTICS ||
+ qc->length != len - 2)
+ return false;
+
+ present = ieee80211_qos_char_presence(qc);
+
+ if (present & ~GENMASK(7, 0))
+ return false;
+
+ if (ieee80211_qos_char_direction(qc) > IEEE80211_QOS_CHAR_DIR_DIRECT)
+ return false;
+
+ /* The LinkID refers to the Service Start Time, which must be present */
+ if (present & IEEE80211_QOS_CHAR_PRES_SERVICE_START_LINK_ID &&
+ !(present & IEEE80211_QOS_CHAR_PRES_SERVICE_START_TIME))
+ return false;
+
+ if (present & IEEE80211_QOS_CHAR_PRES_MAX_MSDU_SIZE)
+ needed += 2;
+ if (present & IEEE80211_QOS_CHAR_PRES_SERVICE_START_TIME)
+ needed += 4;
+ if (present & IEEE80211_QOS_CHAR_PRES_SERVICE_START_LINK_ID)
+ needed += 1;
+ if (present & IEEE80211_QOS_CHAR_PRES_MEAN_DATA_RATE)
+ needed += 3;
+ if (present & IEEE80211_QOS_CHAR_PRES_BURST_SIZE)
+ needed += 4;
+ if (present & IEEE80211_QOS_CHAR_PRES_MSDU_LIFETIME)
+ needed += 2;
+ if (present & IEEE80211_QOS_CHAR_PRES_MSDU_DELIVERY_INFO)
+ needed += 1;
+ if (present & IEEE80211_QOS_CHAR_PRES_MEDIUM_TIME)
+ needed += 2;
+
+ return len == needed;
+}
+
#endif /* LINUX_IEEE80211_EHT_H */
diff --git a/include/linux/ieee80211.h b/include/linux/ieee80211.h
index 7fbc51f873ff1..4990db36cd540 100644
--- a/include/linux/ieee80211.h
+++ b/include/linux/ieee80211.h
@@ -1501,6 +1501,9 @@ enum ieee80211_statuscode {
WLAN_STATUS_NO_DIRECT_LINK = 48,
WLAN_STATUS_STA_NOT_PRESENT = 49,
WLAN_STATUS_STA_NOT_QSTA = 50,
+ WLAN_STATUS_REQUESTED_TCLAS_NOT_SUPPORTED_BY_AP = 56,
+ WLAN_STATUS_INSUFFICIENT_TCLAS_PROCESSING_RESOURCES = 57,
+ WLAN_STATUS_TCLAS_PROCESSING_TERMINATED = 97,
/* 802.11s */
WLAN_STATUS_ANTI_CLOG_REQUIRED = 76,
WLAN_STATUS_FCG_NOT_SUPP = 78,
@@ -1821,10 +1824,12 @@ enum ieee80211_eid_ext {
WLAN_EID_EXT_SHORT_SSID_LIST = 58,
WLAN_EID_EXT_HE_6GHZ_CAPA = 59,
WLAN_EID_EXT_UL_MU_POWER_CAPA = 60,
+ WLAN_EID_EXT_TCLAS_MASK = 89,
WLAN_EID_EXT_EHT_OPERATION = 106,
WLAN_EID_EXT_EHT_MULTI_LINK = 107,
WLAN_EID_EXT_EHT_CAPABILITY = 108,
WLAN_EID_EXT_TID_TO_LINK_MAPPING = 109,
+ WLAN_EID_EXT_QOS_CHARACTERISTICS = 113,
WLAN_EID_EXT_BANDWIDTH_INDICATION = 135,
WLAN_EID_EXT_KNOWN_STA_IDENTIFCATION = 136,
WLAN_EID_EXT_NON_AP_STA_REG_CON = 137,
diff --git a/include/net/cfg80211.h b/include/net/cfg80211.h
index 9aa7d2d4a1841..58b28c53a86b6 100644
--- a/include/net/cfg80211.h
+++ b/include/net/cfg80211.h
@@ -4069,6 +4069,249 @@ struct cfg80211_qos_map {
struct cfg80211_dscp_range up[8];
};
+/**
+ * enum cfg80211_tclas_processing - how the TCLAS elements relate
+ *
+ * @CFG80211_TCLAS_PROCESSING_ALL: an MSDU has to match every element
+ * @CFG80211_TCLAS_PROCESSING_ANY: one matching element is enough
+ * @CFG80211_TCLAS_PROCESSING_DEFAULT: matches every MSDU that no other
+ * descriptor matches, and carries no element
+ * @CFG80211_TCLAS_PROCESSING_ABSENT: the descriptor carries no TCLAS
+ * Processing element. Not a value of the Processing field.
+ */
+enum cfg80211_tclas_processing {
+ CFG80211_TCLAS_PROCESSING_ALL = 0,
+ CFG80211_TCLAS_PROCESSING_ANY = 1,
+ CFG80211_TCLAS_PROCESSING_DEFAULT = 2,
+
+ CFG80211_TCLAS_PROCESSING_ABSENT = 0xff,
+};
+
+/**
+ * enum cfg80211_flow_field - classifier parameter of one MSDU
+ *
+ * A set of these is held as a bitmap of BIT(field) in a u32.
+ *
+ * @CFG80211_FLOW_F_ETH_SA: Ethernet source address
+ * @CFG80211_FLOW_F_ETH_DA: Ethernet destination address
+ * @CFG80211_FLOW_F_ETH_TYPE: ethertype
+ * @CFG80211_FLOW_F_VLAN_PCP: IEEE 802.1Q priority code point
+ * @CFG80211_FLOW_F_VLAN_DEI: IEEE 802.1Q drop eligibility indicator
+ * @CFG80211_FLOW_F_VLAN_VID: IEEE 802.1Q VLAN identifier
+ * @CFG80211_FLOW_F_IP_VERSION: IP version
+ * @CFG80211_FLOW_F_IP_SRC: IP source address
+ * @CFG80211_FLOW_F_IP_DST: IP destination address
+ * @CFG80211_FLOW_F_SRC_PORT: layer 4 source port
+ * @CFG80211_FLOW_F_DST_PORT: layer 4 destination port
+ * @CFG80211_FLOW_F_DSCP: differentiated services code point
+ * @CFG80211_FLOW_F_PROTO: IPv4 protocol or IPv6 next header
+ * @CFG80211_FLOW_F_FLOW_LABEL: IPv6 flow label
+ * @NUM_CFG80211_FLOW_FIELDS: number of fields
+ */
+enum cfg80211_flow_field {
+ CFG80211_FLOW_F_ETH_SA,
+ CFG80211_FLOW_F_ETH_DA,
+ CFG80211_FLOW_F_ETH_TYPE,
+ CFG80211_FLOW_F_VLAN_PCP,
+ CFG80211_FLOW_F_VLAN_DEI,
+ CFG80211_FLOW_F_VLAN_VID,
+ CFG80211_FLOW_F_IP_VERSION,
+ CFG80211_FLOW_F_IP_SRC,
+ CFG80211_FLOW_F_IP_DST,
+ CFG80211_FLOW_F_SRC_PORT,
+ CFG80211_FLOW_F_DST_PORT,
+ CFG80211_FLOW_F_DSCP,
+ CFG80211_FLOW_F_PROTO,
+ CFG80211_FLOW_F_FLOW_LABEL,
+
+ NUM_CFG80211_FLOW_FIELDS,
+};
+
+/**
+ * struct cfg80211_flow_key - masked classifier parameters of one MSDU
+ *
+ * Unselected fields are zero. The whole structure is hashed and compared as a
+ * byte string, so it must contain no undefined octet.
+ *
+ * @src: IP source address, IPv4 in the first four octets
+ * @dst: IP destination address, IPv4 in the first four octets
+ * @flow_label: IPv6 flow label
+ * @sa: Ethernet source address
+ * @da: Ethernet destination address
+ * @eth_type: ethertype
+ * @vlan_tci: IEEE 802.1Q tag control information
+ * @src_port: layer 4 source port
+ * @dst_port: layer 4 destination port
+ * @ip_version: IP version, which keeps IPv4 and IPv6 keys apart
+ * @dscp: differentiated services code point, in the six LSBs
+ * @proto: IPv4 protocol or IPv6 next header
+ * @pad: must be zero
+ */
+struct cfg80211_flow_key {
+ struct in6_addr src;
+ struct in6_addr dst;
+ __be32 flow_label;
+ u8 sa[ETH_ALEN];
+ u8 da[ETH_ALEN];
+ __be16 eth_type;
+ __be16 vlan_tci;
+ __be16 src_port;
+ __be16 dst_port;
+ u8 ip_version;
+ u8 dscp;
+ u8 proto;
+ u8 pad;
+};
+
+/* Longest Filter Value and Filter Mask a filter classifier may carry */
+#define CFG80211_TCLAS_FILTER_LEN 28
+
+/**
+ * struct cfg80211_tclas_filter - octet filter on a protocol payload
+ *
+ * @protocol: IPv4 protocol or IPv6 next header value that locates the
+ * payload to compare
+ * @instance: which occurrence of @protocol to use, counted from zero
+ * @len: length of @value and @mask
+ * @value: octets to compare against the payload
+ * @mask: the bits of @value that are compared
+ */
+struct cfg80211_tclas_filter {
+ u8 protocol;
+ u8 instance;
+ u8 len;
+ u8 value[CFG80211_TCLAS_FILTER_LEN];
+ u8 mask[CFG80211_TCLAS_FILTER_LEN];
+};
+
+/**
+ * struct cfg80211_tclas - parsed TCLAS element
+ *
+ * Only the classifier types the kernel can evaluate are represented, see
+ * cfg80211_parse_tclas().
+ *
+ * @type: classifier type
+ * @fields: the parameters the classifier mask selects, as a bitmap of
+ * &enum cfg80211_flow_field. Zero for a filter classifier, which
+ * selects no field of its own.
+ * @key: the required values, with unselected fields zero, so that a match is
+ * a comparison of two keys
+ * @filter: the octet filter, when @type is a filter classifier
+ */
+struct cfg80211_tclas {
+ u8 type;
+ u32 fields;
+ union {
+ struct cfg80211_flow_key key;
+ struct cfg80211_tclas_filter filter;
+ };
+};
+
+/**
+ * struct cfg80211_flow_info - classifier parameters of one MSDU
+ *
+ * @key: every parameter the source carries, with nothing masked out
+ * @present: bitmap of &enum cfg80211_flow_field, the parameters the source
+ * actually carries. A classifier that names an absent parameter cannot
+ * match.
+ * @skb: the frame the parameters came from, or %NULL when the caller
+ * describes a flow rather than a frame. A filter classifier reads the
+ * frame again and cannot be evaluated without it.
+ * @l3_off: offset of the IP header in @skb
+ */
+struct cfg80211_flow_info {
+ struct cfg80211_flow_key key;
+ u32 present;
+ const struct sk_buff *skb;
+ u16 l3_off;
+};
+
+bool cfg80211_flow_parse(struct sk_buff *skb, struct cfg80211_flow_info *info);
+
+/**
+ * enum cfg80211_flow_dir - direction of a built flow key
+ *
+ * @CFG80211_FLOW_AS_IS: the key describes the frame that was parsed
+ * @CFG80211_FLOW_MIRRORED: the key describes the reverse direction, with the
+ * addresses and the ports swapped
+ */
+enum cfg80211_flow_dir {
+ CFG80211_FLOW_AS_IS,
+ CFG80211_FLOW_MIRRORED,
+};
+
+bool cfg80211_flow_key_build(const struct cfg80211_flow_info *info, u32 fields,
+ enum cfg80211_flow_dir dir,
+ struct cfg80211_flow_key *key);
+
+/**
+ * struct cfg80211_scs_desc - one SCS descriptor
+ *
+ * @req_type: add, remove or change
+ * @tclas_processing: how the elements of @tclas relate
+ * @qos_char: the whole QoS Characteristics element, or %NULL. Set only
+ * after ieee80211_qos_char_size_ok() passed, so the accessors in
+ * <linux/ieee80211-eht.h> may read it.
+ * @id: SCSID, 1 to 255
+ * @up: user priority to assign to a matching MSDU
+ * @qos_char_len: length of @qos_char
+ * @n_tclas: number of entries in @tclas
+ * @tclas: the classifier
+ */
+struct cfg80211_scs_desc {
+ enum nl80211_scs_req_type req_type;
+ enum cfg80211_tclas_processing tclas_processing;
+ const struct ieee80211_qos_char_elem *qos_char;
+ u8 id;
+ u8 up;
+ u16 qos_char_len;
+ u8 n_tclas;
+ struct cfg80211_tclas tclas[] __counted_by(n_tclas);
+};
+
+/**
+ * struct cfg80211_scs_result - result for one SCS descriptor
+ *
+ * @status: IEEE status code
+ */
+struct cfg80211_scs_result {
+ u16 status;
+};
+
+/**
+ * struct cfg80211_mscs_desc - the MSCS of one peer
+ *
+ * @req_type: add, remove or change
+ * @up_bitmap: user priorities that the AP learns from, one bit each
+ * @up_limit: ceiling for the assigned user priority, 0 to 7
+ * @stream_timeout: minimum lifetime of a learned value, in TUs
+ * @fields: classifier parameters, a bitmap of &enum cfg80211_flow_field
+ */
+struct cfg80211_mscs_desc {
+ enum nl80211_scs_req_type req_type;
+ u8 up_bitmap;
+ u8 up_limit;
+ u32 stream_timeout;
+ u32 fields;
+};
+
+/**
+ * struct cfg80211_scs_verdict - result of an SCS evaluation
+ *
+ * @match: an SCS descriptor matched the MSDU
+ * @scsid: identifier of the matching descriptor
+ * @up: user priority to assign
+ */
+struct cfg80211_scs_verdict {
+ bool match;
+ u8 scsid;
+ u8 up;
+};
+
+void cfg80211_scs_evaluate(struct cfg80211_scs_desc * const *desc, u8 n_desc,
+ const struct cfg80211_flow_info *info,
+ struct cfg80211_scs_verdict *verdict);
+
/**
* DOC: Neighbor Awareness Networking (NAN)
*
@@ -5154,6 +5397,13 @@ struct mgmt_frame_regs {
*
* @set_qos_map: Set QoS mapping information to the driver
*
+ * @set_scs: Add, change or remove SCS descriptors of one peer. Userspace
+ * handles the action frame exchange. Each descriptor is answered with
+ * a status code in the result entry of the same index. A declined add
+ * or change leaves the previous classification of that SCSID in force.
+ * A negative return fails the request as a whole.
+ * @set_mscs: Install, change or remove the MSCS of one peer.
+ *
* @set_ap_chanwidth: Set the AP (including P2P GO) mode channel width for the
* given interface This is used e.g. for dynamic HT 20/40 MHz channel width
* changes during the lifetime of the BSS.
@@ -5558,6 +5808,13 @@ struct cfg80211_ops {
struct net_device *dev,
struct cfg80211_qos_map *qos_map);
+ int (*set_scs)(struct wiphy *wiphy, struct net_device *dev,
+ const u8 *peer,
+ struct cfg80211_scs_desc * const *desc,
+ struct cfg80211_scs_result *res, u8 n_desc);
+ int (*set_mscs)(struct wiphy *wiphy, struct net_device *dev,
+ const u8 *peer, struct cfg80211_mscs_desc *desc);
+
int (*set_ap_chanwidth)(struct wiphy *wiphy, struct net_device *dev,
unsigned int link_id,
struct cfg80211_chan_def *chandef);
diff --git a/include/net/mac80211.h b/include/net/mac80211.h
index d6f3c58559101..c65ebbb3265c3 100644
--- a/include/net/mac80211.h
+++ b/include/net/mac80211.h
@@ -5082,6 +5082,34 @@ struct ieee80211_ops {
struct cfg80211_pmsr_request *request);
void (*abort_pmsr)(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
struct cfg80211_pmsr_request *request);
+ /**
+ * @sta_set_scs:
+ * Called with the SCS descriptors that a station requested, after
+ * cfg80211 validated them. Optional, only reached when the wiphy
+ * advertises %NL80211_EXT_FEATURE_SCS. Without it, every descriptor
+ * with a QoS Characteristics element is declined.
+ *
+ * - The results arrive zeroed, which means accepted. Write only
+ * refusals, in the result entry of the same index.
+ * - Decline a descriptor only when its traffic description cannot
+ * be served. mac80211 classifies frames in software either way.
+ * - A declined add or change keeps the previous descriptor of that
+ * SCSID. An accepted one without a traffic description replaces
+ * the device state of that SCSID.
+ * - A removal only drops driver state, the result is ignored. A
+ * station going away drops its rules without a call.
+ * - After a hardware restart every stored rule is passed again as
+ * an add, one per call, and the result is ignored.
+ * - A negative return fails the whole request and leaves the rule
+ * set unchanged, so the driver must leave the device as it was.
+ * - The descriptors belong to the MLD, not to one link.
+ *
+ * Called with the wiphy mutex held. This callback may sleep.
+ */
+ int (*sta_set_scs)(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
+ struct ieee80211_sta *sta,
+ struct cfg80211_scs_desc * const *desc,
+ struct cfg80211_scs_result *res, u8 n_desc);
int (*set_tid_config)(struct ieee80211_hw *hw,
struct ieee80211_vif *vif,
struct ieee80211_sta *sta,
diff --git a/include/uapi/linux/nl80211.h b/include/uapi/linux/nl80211.h
index 9a2ccb8d66b86..ecb11739c5b7d 100644
--- a/include/uapi/linux/nl80211.h
+++ b/include/uapi/linux/nl80211.h
@@ -1427,6 +1427,18 @@
* @NL80211_CMD_STOP_PD: Stop the PD operation, identified by
* its %NL80211_ATTR_WDEV interface.
*
+ * @NL80211_CMD_SET_SCS: Add, change or remove stream classification service
+ * descriptors of the peer given by %NL80211_ATTR_MAC, as requested in an
+ * SCS Request frame. The descriptors are in
+ * %NL80211_ATTR_SCS_DESCRIPTORS. The reply repeats the attribute with
+ * the identifier and the status code of each descriptor. A declined
+ * descriptor does not make the command fail.
+ * @NL80211_CMD_SET_MSCS: Install, change or remove the mirrored stream
+ * classification service of the peer given by %NL80211_ATTR_MAC, as
+ * requested in an MSCS Request frame or a (Re)Association Request. The
+ * parameters are in %NL80211_ATTR_MSCS_DESCRIPTOR. The reply carries
+ * the status code in %NL80211_ATTR_STATUS_CODE.
+ *
* @NL80211_CMD_MAX: highest used command number
* @__NL80211_CMD_AFTER_LAST: internal use
*/
@@ -1705,6 +1717,9 @@ enum nl80211_commands {
NL80211_CMD_START_PD,
NL80211_CMD_STOP_PD,
+ NL80211_CMD_SET_SCS,
+ NL80211_CMD_SET_MSCS,
+
/* add new commands above here */
/* used to define NL80211_CMD_MAX below */
@@ -3190,6 +3205,12 @@ enum nl80211_commands {
* known station to transmit a frame. This is relevant to know whether
* MLD address translation happened or to disable it when sending a frame.
*
+ * @NL80211_ATTR_SCS_DESCRIPTORS: Nested array of SCS descriptors, each one a
+ * nested set of &enum nl80211_scs_desc_attr attributes. Used with
+ * %NL80211_CMD_SET_SCS in both directions.
+ * @NL80211_ATTR_MSCS_DESCRIPTOR: Nested set of &enum nl80211_mscs_desc_attr
+ * attributes. Used with %NL80211_CMD_SET_MSCS.
+ *
* @NUM_NL80211_ATTR: total number of nl80211_attrs available
* @NL80211_ATTR_MAX: highest attribute number currently defined
* @__NL80211_ATTR_AFTER_LAST: internal use
@@ -3792,6 +3813,9 @@ enum nl80211_attrs {
NL80211_ATTR_FRAME_NO_STA,
+ NL80211_ATTR_SCS_DESCRIPTORS,
+ NL80211_ATTR_MSCS_DESCRIPTOR,
+
/* add attributes here, update the policy in nl80211.c */
__NL80211_ATTR_AFTER_LAST,
@@ -6077,6 +6101,87 @@ enum nl80211_tx_power_setting {
NL80211_TX_POWER_FIXED,
};
+/**
+ * enum nl80211_scs_req_type - SCS or MSCS request type
+ *
+ * The values match the Request Type field of the descriptor on the air.
+ *
+ * @NL80211_SCS_REQ_ADD: create the stream, replacing an active one of the
+ * same identifier
+ * @NL80211_SCS_REQ_REMOVE: terminate the stream
+ * @NL80211_SCS_REQ_CHANGE: modify an active stream, leaving it in force if
+ * the request is declined
+ */
+enum nl80211_scs_req_type {
+ NL80211_SCS_REQ_ADD,
+ NL80211_SCS_REQ_REMOVE,
+ NL80211_SCS_REQ_CHANGE,
+};
+
+/**
+ * enum nl80211_scs_desc_attr - one SCS descriptor
+ *
+ * @__NL80211_SCS_DESC_ATTR_INVALID: invalid
+ * @NL80211_SCS_DESC_ATTR_ID: SCSID (u8, 1 to 255)
+ * @NL80211_SCS_DESC_ATTR_REQ_TYPE: request type (u8, see
+ * &enum nl80211_scs_req_type)
+ * @NL80211_SCS_DESC_ATTR_UP: user priority to assign to a matching MSDU
+ * (u8, 0 to 7), from the Intra-Access Category Priority element.
+ * Required for a downlink descriptor or one without
+ * %NL80211_SCS_DESC_ATTR_QOS_CHAR, and rejected otherwise.
+ * @NL80211_SCS_DESC_ATTR_TCLAS: one or more TCLAS elements and at most one
+ * TCLAS Processing element (binary), as received over the air
+ * @NL80211_SCS_DESC_ATTR_QOS_CHAR: the whole QoS Characteristics element
+ * (binary), header included, as received over the air
+ * @NL80211_SCS_DESC_ATTR_STATUS: IEEE status code (u16), kernel to userspace
+ *
+ * @__NL80211_SCS_DESC_ATTR_AFTER_LAST: internal use
+ * @NL80211_SCS_DESC_ATTR_MAX: highest attribute
+ */
+enum nl80211_scs_desc_attr {
+ __NL80211_SCS_DESC_ATTR_INVALID,
+ NL80211_SCS_DESC_ATTR_ID,
+ NL80211_SCS_DESC_ATTR_REQ_TYPE,
+ NL80211_SCS_DESC_ATTR_UP,
+ NL80211_SCS_DESC_ATTR_TCLAS,
+ NL80211_SCS_DESC_ATTR_QOS_CHAR,
+ NL80211_SCS_DESC_ATTR_STATUS,
+
+ __NL80211_SCS_DESC_ATTR_AFTER_LAST,
+ NL80211_SCS_DESC_ATTR_MAX = __NL80211_SCS_DESC_ATTR_AFTER_LAST - 1
+};
+
+/**
+ * enum nl80211_mscs_desc_attr - the MSCS of one peer
+ *
+ * @__NL80211_MSCS_DESC_ATTR_INVALID: invalid
+ * @NL80211_MSCS_DESC_ATTR_REQ_TYPE: request type (u8, see
+ * &enum nl80211_scs_req_type)
+ * @NL80211_MSCS_DESC_ATTR_UP_BITMAP: user priorities that the AP learns
+ * from, one bit each (u8)
+ * @NL80211_MSCS_DESC_ATTR_UP_LIMIT: ceiling for the assigned user priority
+ * (u8, 0 to 7)
+ * @NL80211_MSCS_DESC_ATTR_STREAM_TIMEOUT: minimum lifetime of a learned
+ * value, in TUs (u32)
+ * @NL80211_MSCS_DESC_ATTR_TCLAS_MASK: one or more TCLAS Mask elements
+ * (binary), as received over the air. Required for add and change,
+ * rejected for remove.
+ *
+ * @__NL80211_MSCS_DESC_ATTR_AFTER_LAST: internal use
+ * @NL80211_MSCS_DESC_ATTR_MAX: highest attribute
+ */
+enum nl80211_mscs_desc_attr {
+ __NL80211_MSCS_DESC_ATTR_INVALID,
+ NL80211_MSCS_DESC_ATTR_REQ_TYPE,
+ NL80211_MSCS_DESC_ATTR_UP_BITMAP,
+ NL80211_MSCS_DESC_ATTR_UP_LIMIT,
+ NL80211_MSCS_DESC_ATTR_STREAM_TIMEOUT,
+ NL80211_MSCS_DESC_ATTR_TCLAS_MASK,
+
+ __NL80211_MSCS_DESC_ATTR_AFTER_LAST,
+ NL80211_MSCS_DESC_ATTR_MAX = __NL80211_MSCS_DESC_ATTR_AFTER_LAST - 1
+};
+
/**
* enum nl80211_tid_config - TID config state
* @NL80211_TID_CONFIG_ENABLE: Enable config for the TID
@@ -7125,6 +7230,10 @@ enum nl80211_feature_flags {
* @NL80211_EXT_FEATURE_FAST_ROAM_OFFLOAD: Driver supports fast roaming
* offload in station mode, including Fast Transition or Opportunistic
* Key Caching.
+ * @NL80211_EXT_FEATURE_SCS: Driver supports the stream classification
+ * service, configured with %NL80211_CMD_SET_SCS.
+ * @NL80211_EXT_FEATURE_MSCS: Driver supports the mirrored stream
+ * classification service, configured with %NL80211_CMD_SET_MSCS.
*
* @NUM_NL80211_EXT_FEATURES: number of extended features.
* @MAX_NL80211_EXT_FEATURES: highest extended feature index.
@@ -7209,6 +7318,8 @@ enum nl80211_ext_feature_index {
NL80211_EXT_FEATURE_SET_KEY_LTF_SEED,
NL80211_EXT_FEATURE_PROBE_AP,
NL80211_EXT_FEATURE_FAST_ROAM_OFFLOAD,
+ NL80211_EXT_FEATURE_SCS,
+ NL80211_EXT_FEATURE_MSCS,
/* add new features before the definition below */
NUM_NL80211_EXT_FEATURES,
diff --git a/net/mac80211/Makefile b/net/mac80211/Makefile
index 36f1e3e2222b4..8f8729e44c5f1 100644
--- a/net/mac80211/Makefile
+++ b/net/mac80211/Makefile
@@ -35,7 +35,8 @@ mac80211-y := \
tdls.o \
ocb.o \
airtime.o \
- eht.o uhr.o nan.o
+ eht.o uhr.o nan.o \
+ scs.o
mac80211-$(CONFIG_MAC80211_LEDS) += led.o
mac80211-$(CONFIG_MAC80211_DEBUGFS) += \
diff --git a/net/mac80211/cfg.c b/net/mac80211/cfg.c
index a1753335eb9d0..04c5947718c3c 100644
--- a/net/mac80211/cfg.c
+++ b/net/mac80211/cfg.c
@@ -6073,6 +6073,8 @@ const struct cfg80211_ops mac80211_config_ops = {
.end_cac = ieee80211_end_cac,
.channel_switch = ieee80211_channel_switch,
.set_qos_map = ieee80211_set_qos_map,
+ .set_scs = ieee80211_set_scs,
+ .set_mscs = ieee80211_set_mscs,
.set_ap_chanwidth = ieee80211_set_ap_chanwidth,
.add_tx_ts = ieee80211_add_tx_ts,
.del_tx_ts = ieee80211_del_tx_ts,
diff --git a/net/mac80211/debugfs_sta.c b/net/mac80211/debugfs_sta.c
index ef75255d47d5a..bcf4961af24f4 100644
--- a/net/mac80211/debugfs_sta.c
+++ b/net/mac80211/debugfs_sta.c
@@ -10,6 +10,7 @@
#include <linux/debugfs.h>
#include <linux/ieee80211.h>
+#include <linux/if_vlan.h>
#include "ieee80211_i.h"
#include "debugfs.h"
#include "debugfs_sta.h"
@@ -131,6 +132,243 @@ static ssize_t sta_last_seq_ctrl_read(struct file *file, char __user *userbuf,
}
STA_OPS(last_seq_ctrl);
+static void sta_flow_key_show(struct seq_file *s,
+ const struct cfg80211_flow_key *key, u32 fields)
+{
+ u16 tci = be16_to_cpu(key->vlan_tci);
+
+ if (fields & BIT(CFG80211_FLOW_F_ETH_SA))
+ seq_printf(s, " sa=%pM", key->sa);
+ if (fields & BIT(CFG80211_FLOW_F_ETH_DA))
+ seq_printf(s, " da=%pM", key->da);
+ if (fields & BIT(CFG80211_FLOW_F_ETH_TYPE))
+ seq_printf(s, " ethertype=0x%04x", be16_to_cpu(key->eth_type));
+
+ if (fields & BIT(CFG80211_FLOW_F_VLAN_PCP))
+ seq_printf(s, " pcp=%u",
+ (tci & VLAN_PRIO_MASK) >> VLAN_PRIO_SHIFT);
+ if (fields & BIT(CFG80211_FLOW_F_VLAN_DEI))
+ seq_printf(s, " dei=%u", !!(tci & VLAN_CFI_MASK));
+ if (fields & BIT(CFG80211_FLOW_F_VLAN_VID))
+ seq_printf(s, " vid=%u", tci & VLAN_VID_MASK);
+
+ if (fields & BIT(CFG80211_FLOW_F_IP_VERSION))
+ seq_printf(s, " ipv%u", key->ip_version);
+
+ if (fields & BIT(CFG80211_FLOW_F_IP_SRC))
+ seq_printf(s, key->ip_version == 6 ? " src=%pI6" : " src=%pI4",
+ &key->src);
+ if (fields & BIT(CFG80211_FLOW_F_IP_DST))
+ seq_printf(s, key->ip_version == 6 ? " dst=%pI6" : " dst=%pI4",
+ &key->dst);
+
+ if (fields & BIT(CFG80211_FLOW_F_SRC_PORT))
+ seq_printf(s, " sport=%u", be16_to_cpu(key->src_port));
+ if (fields & BIT(CFG80211_FLOW_F_DST_PORT))
+ seq_printf(s, " dport=%u", be16_to_cpu(key->dst_port));
+
+ if (fields & BIT(CFG80211_FLOW_F_DSCP))
+ seq_printf(s, " dscp=%u", key->dscp);
+ if (fields & BIT(CFG80211_FLOW_F_PROTO))
+ seq_printf(s, " proto=%u", key->proto);
+ if (fields & BIT(CFG80211_FLOW_F_FLOW_LABEL))
+ seq_printf(s, " flowlabel=0x%05x",
+ be32_to_cpu(key->flow_label));
+}
+
+/* The set can be replaced during a walk, so look it up again at every step */
+static void *sta_scs_seq_start(struct seq_file *s, loff_t *pos)
+ __acquires(RCU)
+{
+ struct sta_info *sta = s->private;
+ struct ieee80211_scs_sta *scs;
+
+ rcu_read_lock();
+ scs = rcu_dereference(sta->scs);
+ if (!scs || *pos >= scs->n_rules)
+ return NULL;
+
+ return scs->rule[*pos];
+}
+
+static void *sta_scs_seq_next(struct seq_file *s, void *v, loff_t *pos)
+{
+ struct sta_info *sta = s->private;
+ struct ieee80211_scs_sta *scs = rcu_dereference(sta->scs);
+
+ ++*pos;
+ if (!scs || *pos >= scs->n_rules)
+ return NULL;
+
+ return scs->rule[*pos];
+}
+
+static void sta_scs_seq_stop(struct seq_file *s, void *v)
+ __releases(RCU)
+{
+ rcu_read_unlock();
+}
+
+static int sta_scs_seq_show(struct seq_file *s, void *v)
+{
+ static const char * const processing[] = {
+ [CFG80211_TCLAS_PROCESSING_ALL] = "all",
+ [CFG80211_TCLAS_PROCESSING_ANY] = "any",
+ [CFG80211_TCLAS_PROCESSING_DEFAULT] = "default",
+ };
+ const struct cfg80211_scs_desc *desc = v;
+ const char *proc = "none";
+ u8 i;
+
+ if (desc->tclas_processing < ARRAY_SIZE(processing))
+ proc = processing[desc->tclas_processing];
+
+ seq_printf(s, "scsid %u up %u processing %s", desc->id, desc->up, proc);
+
+ if (desc->qos_char)
+ seq_printf(s, " qos_char %u direction %u", desc->qos_char_len,
+ ieee80211_qos_char_direction(desc->qos_char));
+
+ seq_putc(s, '\n');
+
+ for (i = 0; i < desc->n_tclas; i++) {
+ const struct cfg80211_tclas *t = &desc->tclas[i];
+
+ seq_printf(s, " type %u", t->type);
+ sta_flow_key_show(s, &t->key, t->fields);
+ seq_putc(s, '\n');
+ }
+
+ return 0;
+}
+
+static const struct seq_operations sta_scs_seq_ops = {
+ .start = sta_scs_seq_start,
+ .next = sta_scs_seq_next,
+ .stop = sta_scs_seq_stop,
+ .show = sta_scs_seq_show,
+};
+
+static int sta_scs_open(struct inode *inode, struct file *file)
+{
+ struct seq_file *s;
+ int ret;
+
+ ret = seq_open(file, &sta_scs_seq_ops);
+ if (ret)
+ return ret;
+
+ s = file->private_data;
+ s->private = inode->i_private;
+
+ return 0;
+}
+
+static const struct file_operations sta_scs_ops = {
+ .open = sta_scs_open,
+ .read = seq_read,
+ .llseek = seq_lseek,
+ .release = seq_release,
+};
+
+struct sta_mscs_iter {
+ struct sta_info *sta;
+ struct ieee80211_mscs_sta *mscs;
+};
+
+/* The entry list is not an RCU list, so the whole walk holds the lock */
+static void *sta_mscs_seq_start(struct seq_file *s, loff_t *pos)
+ __acquires(RCU)
+{
+ struct sta_mscs_iter *iter = s->private;
+
+ rcu_read_lock();
+ iter->mscs = rcu_dereference(iter->sta->mscs);
+ if (!iter->mscs)
+ return NULL;
+
+ spin_lock_bh(&iter->mscs->lock);
+
+ if (*pos == 0)
+ return SEQ_START_TOKEN;
+
+ return seq_list_start(&iter->mscs->entries, *pos - 1);
+}
+
+static void *sta_mscs_seq_next(struct seq_file *s, void *v, loff_t *pos)
+{
+ struct sta_mscs_iter *iter = s->private;
+
+ if (v == SEQ_START_TOKEN) {
+ ++*pos;
+ return seq_list_start(&iter->mscs->entries, 0);
+ }
+
+ return seq_list_next(v, &iter->mscs->entries, pos);
+}
+
+static void sta_mscs_seq_stop(struct seq_file *s, void *v)
+ __releases(RCU)
+{
+ struct sta_mscs_iter *iter = s->private;
+
+ if (iter->mscs)
+ spin_unlock_bh(&iter->mscs->lock);
+
+ rcu_read_unlock();
+}
+
+static int sta_mscs_seq_show(struct seq_file *s, void *v)
+{
+ struct sta_mscs_iter *iter = s->private;
+ struct ieee80211_mscs_sta *mscs = iter->mscs;
+ struct ieee80211_flow_entry *entry;
+
+ if (v == SEQ_START_TOKEN) {
+ seq_printf(s,
+ "up_bitmap 0x%02x up_limit %u timeout %ums entries %u\n",
+ mscs->up_bitmap, mscs->up_limit,
+ jiffies_to_msecs(mscs->timeout), mscs->n_entries);
+ return 0;
+ }
+
+ entry = list_entry(v, struct ieee80211_flow_entry, list);
+
+ seq_printf(s, " up %u age %ums", READ_ONCE(entry->up),
+ jiffies_to_msecs(jiffies - READ_ONCE(entry->last_update)));
+ sta_flow_key_show(s, &entry->hkey.key, mscs->layout);
+ seq_putc(s, '\n');
+
+ return 0;
+}
+
+static const struct seq_operations sta_mscs_seq_ops = {
+ .start = sta_mscs_seq_start,
+ .next = sta_mscs_seq_next,
+ .stop = sta_mscs_seq_stop,
+ .show = sta_mscs_seq_show,
+};
+
+static int sta_mscs_open(struct inode *inode, struct file *file)
+{
+ struct sta_mscs_iter *iter;
+
+ iter = __seq_open_private(file, &sta_mscs_seq_ops, sizeof(*iter));
+ if (!iter)
+ return -ENOMEM;
+
+ iter->sta = inode->i_private;
+
+ return 0;
+}
+
+static const struct file_operations sta_mscs_ops = {
+ .open = sta_mscs_open,
+ .read = seq_read,
+ .llseek = seq_lseek,
+ .release = seq_release_private,
+};
+
#define AQM_TXQ_ENTRY_LEN 130
static ssize_t sta_aqm_read(struct file *file, char __user *userbuf,
@@ -1252,6 +1490,8 @@ void ieee80211_sta_debugfs_add(struct sta_info *sta)
sta->debugfs_dir = debugfs_create_dir(mac, stations_dir);
DEBUGFS_ADD(flags);
+ DEBUGFS_ADD(scs);
+ DEBUGFS_ADD(mscs);
DEBUGFS_ADD(aid);
DEBUGFS_ADD(num_ps_buf_frames);
DEBUGFS_ADD(last_seq_ctrl);
diff --git a/net/mac80211/driver-ops.h b/net/mac80211/driver-ops.h
index f1c0b87fddd5f..f7684e8db0251 100644
--- a/net/mac80211/driver-ops.h
+++ b/net/mac80211/driver-ops.h
@@ -1552,6 +1552,32 @@ static inline void drv_del_nan_func(struct ieee80211_local *local,
trace_drv_return_void(local);
}
+static inline int drv_sta_set_scs(struct ieee80211_local *local,
+ struct ieee80211_sub_if_data *sdata,
+ struct sta_info *sta,
+ struct cfg80211_scs_desc * const *desc,
+ struct cfg80211_scs_result *res, u8 n_desc)
+{
+ int ret;
+
+ might_sleep();
+ lockdep_assert_wiphy(local->hw.wiphy);
+
+ if (!local->ops->sta_set_scs || !sta->uploaded)
+ return 0;
+
+ sdata = get_bss_sdata(sdata);
+ if (!check_sdata_in_driver(sdata))
+ return -EIO;
+
+ trace_drv_sta_set_scs(local, sdata, &sta->sta, n_desc);
+ ret = local->ops->sta_set_scs(&local->hw, &sdata->vif, &sta->sta, desc,
+ res, n_desc);
+ trace_drv_return_int(local, ret);
+
+ return ret;
+}
+
static inline int drv_set_tid_config(struct ieee80211_local *local,
struct ieee80211_sub_if_data *sdata,
struct ieee80211_sta *sta,
diff --git a/net/mac80211/ieee80211_i.h b/net/mac80211/ieee80211_i.h
index 10767666c45c0..e0a7e9bb75a0d 100644
--- a/net/mac80211/ieee80211_i.h
+++ b/net/mac80211/ieee80211_i.h
@@ -345,6 +345,10 @@ struct ieee80211_if_ap {
bool multicast_to_unicast;
bool active;
+
+ /* learned MSCS entries of every station of this BSS, and its VLANs */
+ struct rhashtable flow_tbl;
+ struct wiphy_delayed_work flow_gc_work;
};
struct ieee80211_if_vlan {
@@ -2214,6 +2218,19 @@ void ieee80211_recalc_txpower(struct ieee80211_link_data *link,
bool update_bss);
void ieee80211_recalc_offload(struct ieee80211_local *local);
+/* SCS and MSCS */
+int ieee80211_set_scs(struct wiphy *wiphy, struct net_device *dev,
+ const u8 *peer, struct cfg80211_scs_desc * const *desc,
+ struct cfg80211_scs_result *res, u8 n_desc);
+int ieee80211_set_mscs(struct wiphy *wiphy, struct net_device *dev,
+ const u8 *peer, struct cfg80211_mscs_desc *desc);
+void ieee80211_sta_scs_reconfig(struct sta_info *sta);
+void ieee80211_sta_scs_free(struct sta_info *sta);
+bool ieee80211_flow_classify(struct sta_info *sta, struct sk_buff *skb);
+void ieee80211_flow_learn(struct ieee80211_rx_data *rx);
+int ieee80211_flow_tbl_init(struct ieee80211_sub_if_data *sdata);
+void ieee80211_flow_tbl_destroy(struct ieee80211_sub_if_data *sdata);
+
/* link handling */
void ieee80211_link_setup(struct ieee80211_link_data *link);
void ieee80211_link_init(struct ieee80211_sub_if_data *sdata,
diff --git a/net/mac80211/iface.c b/net/mac80211/iface.c
index 8535dd478633c..7e1f97e1bfcd3 100644
--- a/net/mac80211/iface.c
+++ b/net/mac80211/iface.c
@@ -542,6 +542,8 @@ static void ieee80211_do_stop(struct ieee80211_sub_if_data *sdata, bool going_do
atomic_dec(&local->iff_allmultis);
if (sdata->vif.type == NL80211_IFTYPE_AP) {
+ ieee80211_flow_tbl_destroy(sdata);
+
local->fif_pspoll--;
local->fif_probe_req--;
} else if (sdata->vif.type == NL80211_IFTYPE_ADHOC) {
@@ -1435,6 +1437,10 @@ int ieee80211_do_open(struct wireless_dev *wdev, bool coming_up)
break;
}
case NL80211_IFTYPE_AP:
+ res = ieee80211_flow_tbl_init(sdata);
+ if (res)
+ return res;
+ break;
case NL80211_IFTYPE_MESH_POINT:
case NL80211_IFTYPE_STATION:
case NL80211_IFTYPE_MONITOR:
@@ -1469,7 +1475,7 @@ int ieee80211_do_open(struct wireless_dev *wdev, bool coming_up)
* no need to worry about AP_VLAN/NAN_DATA cleanup since
* in that case we can't have open_count == 0
*/
- return res;
+ goto err_flow_tbl;
}
ieee80211_led_radio(local, true);
ieee80211_mod_tpt_led_trig(local,
@@ -1645,6 +1651,9 @@ int ieee80211_do_open(struct wireless_dev *wdev, bool coming_up)
list_del(&sdata->u.vlan.list);
/* Might not be initialized yet, but it is harmless */
sdata->bss = NULL;
+ err_flow_tbl:
+ if (sdata->vif.type == NL80211_IFTYPE_AP)
+ ieee80211_flow_tbl_destroy(sdata);
return res;
}
diff --git a/net/mac80211/rx.c b/net/mac80211/rx.c
index b3990b7a72990..fb7450715d2d8 100644
--- a/net/mac80211/rx.c
+++ b/net/mac80211/rx.c
@@ -2790,6 +2790,9 @@ ieee80211_deliver_skb(struct ieee80211_rx_data *rx)
u64_stats_update_end(&rx->link_sta->rx_stats.syncp);
}
+ if (rx->sta)
+ ieee80211_flow_learn(rx);
+
if ((sdata->vif.type == NL80211_IFTYPE_AP ||
sdata->vif.type == NL80211_IFTYPE_AP_VLAN) &&
!(sdata->flags & IEEE80211_SDATA_DONT_BRIDGE_PACKETS) &&
@@ -4995,6 +4998,8 @@ static void ieee80211_rx_8023(struct ieee80211_rx_data *rx,
u64_stats_add(&stats->bytes, orig_len);
u64_stats_update_end(&stats->syncp);
+ ieee80211_flow_learn(rx);
+
if (fast_rx->internal_forward) {
struct sk_buff *xmit_skb = NULL;
if (is_multicast_ether_addr(da)) {
diff --git a/net/mac80211/scs.c b/net/mac80211/scs.c
new file mode 100644
index 0000000000000..076e97cddbf1a
--- /dev/null
+++ b/net/mac80211/scs.c
@@ -0,0 +1,574 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Stream classification service (SCS) and mirrored SCS (MSCS)
+ *
+ * Copyright (C) 2026 Felix Fietkau <nbd@nbd.name>
+ */
+#include <linux/ieee80211.h>
+#include <net/cfg80211.h>
+#include "driver-ops.h"
+#include "ieee80211_i.h"
+#include "sta_info.h"
+
+/* An hour, in TUs of 1024 microseconds */
+#define IEEE80211_MSCS_TIMEOUT_MAX_TU 3515625
+
+/* Limit on learned MSCS entries per station */
+#define IEEE80211_MSCS_ENTRIES_MAX 64
+
+/* The hashed bytes must hold no padding between the peer and the key */
+static_assert(offsetofend(struct ieee80211_flow_hkey, key) ==
+ sizeof(struct sta_info *) + sizeof(struct cfg80211_flow_key));
+
+static const struct rhashtable_params flow_rht_params = {
+ .head_offset = offsetof(struct ieee80211_flow_entry, node),
+ .key_offset = offsetof(struct ieee80211_flow_entry, hkey),
+ .key_len = offsetofend(struct ieee80211_flow_hkey, key),
+ .automatic_shrinking = true,
+};
+
+static size_t ieee80211_scs_rule_size(const struct cfg80211_scs_desc *desc)
+{
+ return ALIGN(struct_size(desc, tclas, desc->n_tclas) +
+ desc->qos_char_len, __alignof__(*desc));
+}
+
+static void *ieee80211_scs_rule_copy(const struct cfg80211_scs_desc *src,
+ void *pos)
+{
+ size_t len = struct_size(src, tclas, src->n_tclas);
+ struct cfg80211_scs_desc *dst = pos;
+
+ memcpy(dst, src, len);
+ /* A stored rule is always handed back to the driver as an install */
+ dst->req_type = NL80211_SCS_REQ_ADD;
+ pos += len;
+
+ if (src->qos_char_len) {
+ memcpy(pos, src->qos_char, src->qos_char_len);
+ dst->qos_char = pos;
+ } else {
+ dst->qos_char = NULL;
+ }
+
+ return PTR_ALIGN(pos + src->qos_char_len, __alignof__(*dst));
+}
+
+static u8 ieee80211_scs_rule_find(struct cfg80211_scs_desc * const *rule,
+ u8 n_rules, u8 id)
+{
+ u8 i;
+
+ for (i = 0; i < n_rules; i++)
+ if (rule[i]->id == id)
+ break;
+
+ return i;
+}
+
+static size_t ieee80211_scs_sta_head(unsigned int n_rules)
+{
+ struct ieee80211_scs_sta *scs;
+
+ return ALIGN(struct_size(scs, rule, n_rules),
+ __alignof__(struct cfg80211_scs_desc));
+}
+
+/*
+ * Size for every rule that can end up in the set, so that no allocation
+ * fails after the driver programmed the accepted descriptors.
+ */
+static size_t ieee80211_scs_sta_size(const struct ieee80211_scs_sta *old,
+ struct cfg80211_scs_desc * const *desc,
+ u8 n_desc, unsigned int n_alloc)
+{
+ size_t size = ieee80211_scs_sta_head(n_alloc);
+ unsigned int i;
+
+ for (i = 0; old && i < old->n_rules; i++)
+ size += ieee80211_scs_rule_size(old->rule[i]);
+
+ for (i = 0; i < n_desc; i++)
+ size += ieee80211_scs_rule_size(desc[i]);
+
+ return size;
+}
+
+/*
+ * @rule points into the old set and into the request, so copy each rule.
+ * The block was sized for @n_alloc rules.
+ */
+static void ieee80211_scs_sta_fill(struct ieee80211_scs_sta *scs,
+ struct cfg80211_scs_desc * const *rule,
+ u8 n_rules, unsigned int n_alloc)
+{
+ void *pos = (void *)scs + ieee80211_scs_sta_head(n_alloc);
+ u8 i;
+
+ /* Set before the array is filled, for __counted_by() */
+ scs->n_rules = n_rules;
+
+ for (i = 0; i < n_rules; i++) {
+ scs->rule[i] = pos;
+ pos = ieee80211_scs_rule_copy(rule[i], pos);
+ }
+}
+
+/**
+ * ieee80211_flow_classify - give an MSDU the user priority of its stream
+ *
+ * @sta: the receiver
+ * @skb: the MSDU, in IEEE 802.3 format
+ *
+ * Return: %true when it set skb->priority, %false to leave the frame to the
+ * QoS map.
+ */
+bool ieee80211_flow_classify(struct sta_info *sta, struct sk_buff *skb)
+{
+ struct ieee80211_flow_entry *entry;
+ struct cfg80211_scs_verdict verdict;
+ struct ieee80211_mscs_sta *mscs;
+ struct ieee80211_flow_hkey hkey;
+ struct cfg80211_flow_info info;
+ struct ieee80211_scs_sta *scs;
+
+ scs = rcu_dereference(sta->scs);
+ mscs = rcu_dereference(sta->mscs);
+ if (!scs && !mscs)
+ return false;
+
+ if (!cfg80211_flow_parse(skb, &info))
+ return false;
+
+ if (scs) {
+ cfg80211_scs_evaluate(scs->rule, scs->n_rules, &info,
+ &verdict);
+ if (verdict.match) {
+ skb->priority = verdict.up;
+
+ return true;
+ }
+ }
+
+ /* An SCS match takes precedence over the MSCS */
+ if (!mscs)
+ return false;
+
+ hkey.sta = sta;
+ if (!cfg80211_flow_key_build(&info, mscs->layout, CFG80211_FLOW_AS_IS,
+ &hkey.key))
+ return false;
+
+ entry = rhashtable_lookup(&sta->sdata->bss->flow_tbl, &hkey,
+ flow_rht_params);
+ if (!entry)
+ return false;
+
+ skb->priority = min_t(u8, READ_ONCE(entry->up), mscs->up_limit);
+
+ return true;
+}
+
+static void ieee80211_flow_entry_free(struct ieee80211_if_ap *ap,
+ struct ieee80211_flow_entry *entry)
+{
+ rhashtable_remove_fast(&ap->flow_tbl, &entry->node, flow_rht_params);
+ list_del(&entry->list);
+ kfree_rcu(entry, rcu_head);
+}
+
+/* The caller holds @mscs->lock unless nothing else can reach the list */
+static void ieee80211_flow_purge(struct ieee80211_if_ap *ap,
+ struct ieee80211_mscs_sta *mscs)
+{
+ struct ieee80211_flow_entry *entry, *tmp;
+
+ list_for_each_entry_safe(entry, tmp, &mscs->entries, list)
+ ieee80211_flow_entry_free(ap, entry);
+
+ mscs->n_entries = 0;
+}
+
+/* Return the time until the next entry expires, zero when none is left */
+static unsigned long ieee80211_flow_gc_sta(struct ieee80211_if_ap *ap,
+ struct sta_info *sta)
+{
+ struct ieee80211_flow_entry *entry, *tmp;
+ struct ieee80211_mscs_sta *mscs;
+ unsigned long delay = 0;
+
+ mscs = wiphy_dereference(sta->local->hw.wiphy, sta->mscs);
+ if (!mscs)
+ return 0;
+
+ spin_lock_bh(&mscs->lock);
+
+ list_for_each_entry_safe(entry, tmp, &mscs->entries, list) {
+ unsigned long dead = READ_ONCE(entry->last_update) +
+ mscs->timeout;
+
+ if (time_after_eq(jiffies, dead)) {
+ ieee80211_flow_entry_free(ap, entry);
+ mscs->n_entries--;
+ continue;
+ }
+
+ if (!delay || time_before(dead, jiffies + delay))
+ delay = dead - jiffies;
+ }
+
+ spin_unlock_bh(&mscs->lock);
+
+ return delay;
+}
+
+static void ieee80211_flow_gc_work(struct wiphy *wiphy, struct wiphy_work *work)
+{
+ struct ieee80211_sub_if_data *sdata;
+ struct ieee80211_if_ap *ap;
+ unsigned long delay = 0;
+ struct sta_info *sta;
+
+ ap = container_of(work, struct ieee80211_if_ap, flow_gc_work.work);
+ sdata = container_of(ap, struct ieee80211_sub_if_data, u.ap);
+
+ list_for_each_entry(sta, &sdata->local->sta_list, list) {
+ unsigned long sta_delay;
+
+ if (sta->sdata->bss != ap)
+ continue;
+
+ sta_delay = ieee80211_flow_gc_sta(ap, sta);
+ if (sta_delay && (!delay || sta_delay < delay))
+ delay = sta_delay;
+ }
+
+ if (delay)
+ wiphy_delayed_work_queue(wiphy, &ap->flow_gc_work, delay);
+}
+
+int ieee80211_flow_tbl_init(struct ieee80211_sub_if_data *sdata)
+{
+ wiphy_delayed_work_init(&sdata->u.ap.flow_gc_work,
+ ieee80211_flow_gc_work);
+
+ return rhashtable_init(&sdata->u.ap.flow_tbl, &flow_rht_params);
+}
+
+static void ieee80211_flow_tbl_free(void *ptr, void *arg)
+{
+ struct ieee80211_flow_entry *entry = ptr;
+
+ kfree(entry);
+}
+
+void ieee80211_flow_tbl_destroy(struct ieee80211_sub_if_data *sdata)
+{
+ wiphy_delayed_work_cancel(sdata->local->hw.wiphy,
+ &sdata->u.ap.flow_gc_work);
+ rhashtable_free_and_destroy(&sdata->u.ap.flow_tbl,
+ ieee80211_flow_tbl_free, NULL);
+}
+
+static void ieee80211_flow_insert(struct ieee80211_sub_if_data *sdata,
+ struct ieee80211_mscs_sta *mscs,
+ const struct ieee80211_flow_hkey *hkey, u8 up)
+{
+ struct ieee80211_flow_entry *entry, *other;
+ bool first;
+
+ entry = kzalloc_obj(*entry, GFP_ATOMIC);
+ if (!entry)
+ return;
+
+ entry->hkey.sta = hkey->sta;
+ entry->hkey.key = hkey->key;
+ entry->up = up;
+ entry->last_update = jiffies;
+
+ spin_lock_bh(&mscs->lock);
+
+ /* Another MSCS may have been installed, which emptied this list */
+ if (rcu_dereference(hkey->sta->mscs) != mscs)
+ goto drop;
+
+ if (mscs->n_entries >= IEEE80211_MSCS_ENTRIES_MAX)
+ goto drop;
+
+ other = rhashtable_lookup_get_insert_fast(&sdata->bss->flow_tbl,
+ &entry->node,
+ flow_rht_params);
+ if (other) {
+ /* Concurrent insert of the same key, the last writer wins */
+ if (!IS_ERR(other)) {
+ WRITE_ONCE(other->up, up);
+ WRITE_ONCE(other->last_update, jiffies);
+ }
+
+ goto drop;
+ }
+
+ list_add_tail(&entry->list, &mscs->entries);
+ first = mscs->n_entries++ == 0;
+ spin_unlock_bh(&mscs->lock);
+
+ /* The work requeues itself while entries remain */
+ if (first)
+ wiphy_delayed_work_queue(sdata->local->hw.wiphy,
+ &sdata->bss->flow_gc_work, 1);
+
+ return;
+
+drop:
+ spin_unlock_bh(&mscs->lock);
+ kfree(entry);
+}
+
+/**
+ * ieee80211_flow_learn - record the user priority of an uplink flow
+ *
+ * @rx: the received frame, in IEEE 802.3 format
+ *
+ * The key is built for the downlink direction, so that the transmit path
+ * can look it up as is.
+ */
+void ieee80211_flow_learn(struct ieee80211_rx_data *rx)
+{
+ struct ieee80211_sub_if_data *sdata = rx->sdata;
+ const struct ethhdr *eth = (void *)rx->skb->data;
+ struct ieee80211_flow_hkey hkey;
+ struct ieee80211_mscs_sta *mscs;
+ struct ieee80211_flow_entry *entry;
+ struct cfg80211_flow_info info;
+ /* The TID from ieee80211_parse_qos(), or from a decap offload driver */
+ u32 up = rx->skb->priority;
+
+ if (sdata->vif.type != NL80211_IFTYPE_AP &&
+ sdata->vif.type != NL80211_IFTYPE_AP_VLAN)
+ return;
+
+ mscs = rcu_dereference(rx->sta->mscs);
+ if (!mscs)
+ return;
+
+ if (up > 7 || !(mscs->up_bitmap & BIT(up)))
+ return;
+
+ if (is_multicast_ether_addr(eth->h_dest) ||
+ !ether_addr_equal(eth->h_source, rx->sta->addr))
+ return;
+
+ if (!cfg80211_flow_parse(rx->skb, &info))
+ return;
+
+ hkey.sta = rx->sta;
+ if (!cfg80211_flow_key_build(&info, mscs->layout,
+ CFG80211_FLOW_MIRRORED, &hkey.key))
+ return;
+
+ entry = rhashtable_lookup(&sdata->bss->flow_tbl, &hkey,
+ flow_rht_params);
+ if (entry) {
+ WRITE_ONCE(entry->up, up);
+ WRITE_ONCE(entry->last_update, jiffies);
+ return;
+ }
+
+ ieee80211_flow_insert(sdata, mscs, &hkey, up);
+}
+
+int ieee80211_set_scs(struct wiphy *wiphy, struct net_device *dev,
+ const u8 *peer, struct cfg80211_scs_desc * const *desc,
+ struct cfg80211_scs_result *res, u8 n_desc)
+{
+ struct ieee80211_sub_if_data *sdata = IEEE80211_DEV_TO_SUB_IF(dev);
+ struct ieee80211_scs_sta *scs, *old;
+ struct cfg80211_scs_desc **rule;
+ struct sta_info *sta;
+ unsigned int n_alloc;
+ u8 i, n_rules = 0;
+ int ret;
+
+ lockdep_assert_wiphy(wiphy);
+
+ sta = sta_info_get_bss(sdata, peer);
+ if (!sta)
+ return -ENOENT;
+
+ old = wiphy_dereference(wiphy, sta->scs);
+
+ /* One rule per SCSID bounds the size of the new set */
+ n_alloc = (old ? old->n_rules : 0) + n_desc;
+
+ rule = kcalloc(n_alloc, sizeof(*rule), GFP_KERNEL);
+ if (!rule)
+ return -ENOMEM;
+
+ /* 255 rules with 255 classifiers each exceed what kmalloc hands out */
+ scs = kvzalloc(ieee80211_scs_sta_size(old, desc, n_desc, n_alloc),
+ GFP_KERNEL);
+ if (!scs) {
+ ret = -ENOMEM;
+ goto free;
+ }
+
+ if (sdata->local->ops->sta_set_scs && sta->uploaded) {
+ ret = drv_sta_set_scs(sdata->local, sdata, sta, desc, res,
+ n_desc);
+ if (ret)
+ goto free;
+ } else {
+ /* Without a driver, no traffic description can be served */
+ for (i = 0; i < n_desc; i++)
+ if (desc[i]->qos_char)
+ res[i].status = WLAN_STATUS_REQUEST_DECLINED;
+ }
+
+ if (old) {
+ n_rules = old->n_rules;
+ memcpy(rule, old->rule, n_rules * sizeof(*rule));
+ }
+
+ for (i = 0; i < n_desc; i++) {
+ u8 at = ieee80211_scs_rule_find(rule, n_rules, desc[i]->id);
+
+ if (desc[i]->req_type == NL80211_SCS_REQ_REMOVE) {
+ /* A removal is answered even when no rule matched */
+ res[i].status = WLAN_STATUS_TCLAS_PROCESSING_TERMINATED;
+
+ if (at < n_rules) {
+ n_rules--;
+ memmove(&rule[at], &rule[at + 1],
+ (n_rules - at) * sizeof(*rule));
+ }
+
+ continue;
+ }
+
+ if (res[i].status != WLAN_STATUS_SUCCESS)
+ continue;
+
+ if (at < n_rules)
+ rule[at] = desc[i];
+ else
+ rule[n_rules++] = desc[i];
+ }
+
+ if (n_rules) {
+ ieee80211_scs_sta_fill(scs, rule, n_rules, n_alloc);
+ } else {
+ kvfree(scs);
+ scs = NULL;
+ }
+
+ kfree(rule);
+
+ rcu_assign_pointer(sta->scs, scs);
+ if (old)
+ kvfree_rcu(old, rcu_head);
+
+ return 0;
+
+free:
+ kvfree(scs);
+ kfree(rule);
+
+ return ret;
+}
+
+int ieee80211_set_mscs(struct wiphy *wiphy, struct net_device *dev,
+ const u8 *peer, struct cfg80211_mscs_desc *desc)
+{
+ struct ieee80211_sub_if_data *sdata = IEEE80211_DEV_TO_SUB_IF(dev);
+ struct ieee80211_mscs_sta *mscs = NULL, *old;
+ struct sta_info *sta;
+ u32 tu;
+
+ lockdep_assert_wiphy(wiphy);
+
+ /* Only an AP provides MSCS */
+ if (!sdata->bss)
+ return -EOPNOTSUPP;
+
+ sta = sta_info_get_bss(sdata, peer);
+ if (!sta)
+ return -ENOENT;
+
+ if (desc->req_type != NL80211_SCS_REQ_REMOVE) {
+ mscs = kzalloc_obj(*mscs);
+ if (!mscs)
+ return -ENOMEM;
+
+ mscs->layout = desc->fields;
+ mscs->up_bitmap = desc->up_bitmap;
+ mscs->up_limit = desc->up_limit;
+
+ /*
+ * Cap at an hour to keep tu * 1024 within the unsigned int
+ * that usecs_to_jiffies() takes. A timeout of zero jiffies
+ * would expire an entry at the next collection.
+ */
+ tu = min(desc->stream_timeout, IEEE80211_MSCS_TIMEOUT_MAX_TU);
+ mscs->timeout = max(1UL, usecs_to_jiffies(tu * 1024));
+
+ spin_lock_init(&mscs->lock);
+ INIT_LIST_HEAD(&mscs->entries);
+ }
+
+ old = wiphy_dereference(wiphy, sta->mscs);
+ rcu_assign_pointer(sta->mscs, mscs);
+
+ if (old) {
+ /* The entries were keyed with the old layout */
+ spin_lock_bh(&old->lock);
+ ieee80211_flow_purge(sta->sdata->bss, old);
+ spin_unlock_bh(&old->lock);
+
+ kfree_rcu(old, rcu_head);
+ }
+
+ return 0;
+}
+
+/*
+ * A restarted device lost every stream, so program them again. A refusal
+ * changes nothing here: the rule stays and mac80211 keeps classifying for it.
+ */
+void ieee80211_sta_scs_reconfig(struct sta_info *sta)
+{
+ struct ieee80211_local *local = sta->local;
+ struct ieee80211_scs_sta *scs;
+ u8 i;
+
+ lockdep_assert_wiphy(local->hw.wiphy);
+
+ scs = wiphy_dereference(local->hw.wiphy, sta->scs);
+ if (!scs)
+ return;
+
+ /* One rule per call, so no result array has to be allocated */
+ for (i = 0; i < scs->n_rules; i++) {
+ struct cfg80211_scs_result res = {};
+
+ drv_sta_set_scs(local, sta->sdata, sta, &scs->rule[i], &res, 1);
+ }
+}
+
+void ieee80211_sta_scs_free(struct sta_info *sta)
+{
+ struct ieee80211_mscs_sta *mscs;
+ struct ieee80211_scs_sta *scs;
+
+ /* Nothing can reach the station any more, so no lock is needed */
+ scs = rcu_dereference_raw(sta->scs);
+ RCU_INIT_POINTER(sta->scs, NULL);
+ kvfree(scs);
+
+ mscs = rcu_dereference_raw(sta->mscs);
+ RCU_INIT_POINTER(sta->mscs, NULL);
+ if (!mscs)
+ return;
+
+ ieee80211_flow_purge(sta->sdata->bss, mscs);
+ kfree(mscs);
+}
diff --git a/net/mac80211/sta_info.c b/net/mac80211/sta_info.c
index fdf00cbf49d8b..e14d78b002f1c 100644
--- a/net/mac80211/sta_info.c
+++ b/net/mac80211/sta_info.c
@@ -508,6 +508,7 @@ void sta_info_free(struct ieee80211_local *local, struct sta_info *sta)
kfree(sta->mesh);
#endif
+ ieee80211_sta_scs_free(sta);
sta_info_free_link(&sta->deflink);
kfree(sta);
}
diff --git a/net/mac80211/sta_info.h b/net/mac80211/sta_info.h
index ee0d32877c5b4..6e65313fe4d9c 100644
--- a/net/mac80211/sta_info.h
+++ b/net/mac80211/sta_info.h
@@ -634,6 +634,79 @@ struct ieee80211_sta_removed_link_stats {
} pertid_stats;
};
+/**
+ * struct ieee80211_scs_sta - the SCS rule set of one station
+ *
+ * @rcu_head: for freeing
+ * @n_rules: number of entries in @rule
+ * @rule: the active descriptors, in the order they were installed
+ *
+ * One allocation holds this structure, the descriptors it points to and their
+ * QoS Characteristics octets, so the whole set has one lifetime.
+ */
+struct ieee80211_scs_sta {
+ struct rcu_head rcu_head;
+ u8 n_rules;
+ struct cfg80211_scs_desc *rule[] __counted_by(n_rules);
+};
+
+/**
+ * struct ieee80211_mscs_sta - the MSCS of one station
+ *
+ * @rcu_head: for freeing
+ * @layout: classifier parameters, a bitmap of &enum cfg80211_flow_field
+ * @up_bitmap: user priorities that the AP learns from, one bit each
+ * @up_limit: ceiling for the assigned user priority
+ * @timeout: minimum lifetime of a learned value, in jiffies
+ * @lock: protects @entries and @n_entries
+ * @entries: the learned entries of this station, for teardown
+ * @n_entries: number of entries in @entries
+ */
+struct ieee80211_mscs_sta {
+ struct rcu_head rcu_head;
+ u32 layout;
+ u8 up_bitmap;
+ u8 up_limit;
+ unsigned long timeout;
+
+ spinlock_t lock; /* protects @entries and @n_entries */
+ struct list_head entries;
+ u32 n_entries;
+};
+
+/**
+ * struct ieee80211_flow_hkey - what a learned entry is looked up by
+ *
+ * @sta: the peer, so that a flow of one station never answers for another
+ * @key: the masked tuple, in the downlink direction
+ *
+ * Not packed: the whole of it is compared, but a packed aggregate would force
+ * byte-wise access to @sta.
+ */
+struct ieee80211_flow_hkey {
+ struct sta_info *sta;
+ struct cfg80211_flow_key key;
+};
+
+/**
+ * struct ieee80211_flow_entry - the user priority learned for one flow
+ *
+ * @hkey: what it is looked up by
+ * @node: hash table linkage
+ * @list: linkage in &ieee80211_mscs_sta.entries
+ * @last_update: jiffies of the last uplink frame of this flow
+ * @up: the learned user priority
+ * @rcu_head: for freeing
+ */
+struct ieee80211_flow_entry {
+ struct ieee80211_flow_hkey hkey;
+ struct rhash_head node;
+ struct list_head list;
+ unsigned long last_update;
+ u8 up;
+ struct rcu_head rcu_head;
+};
+
/**
* struct sta_info - STA information
*
@@ -694,6 +767,8 @@ struct ieee80211_sta_removed_link_stats {
*
* @fast_tx: TX fastpath information
* @fast_rx: RX fastpath information
+ * @scs: SCS rule set, or %NULL
+ * @mscs: MSCS parameters, or %NULL
* @tdls_chandef: a TDLS peer can have a wider chandef that is compatible to
* the BSS one.
* @frags: fragment cache
@@ -730,6 +805,9 @@ struct sta_info {
struct ieee80211_fast_tx __rcu *fast_tx;
struct ieee80211_fast_rx __rcu *fast_rx;
+ struct ieee80211_scs_sta __rcu *scs;
+ struct ieee80211_mscs_sta __rcu *mscs;
+
#ifdef CONFIG_MAC80211_MESH
struct mesh_sta *mesh;
#endif
diff --git a/net/mac80211/trace.h b/net/mac80211/trace.h
index 562a4964afa3a..1e6026306fb2e 100644
--- a/net/mac80211/trace.h
+++ b/net/mac80211/trace.h
@@ -1023,6 +1023,33 @@ DECLARE_EVENT_CLASS(sta_event,
)
);
+TRACE_EVENT(drv_sta_set_scs,
+ TP_PROTO(struct ieee80211_local *local,
+ struct ieee80211_sub_if_data *sdata,
+ struct ieee80211_sta *sta, u8 n_desc),
+
+ TP_ARGS(local, sdata, sta, n_desc),
+
+ TP_STRUCT__entry(
+ LOCAL_ENTRY
+ VIF_ENTRY
+ STA_ENTRY
+ __field(u8, n_desc)
+ ),
+
+ TP_fast_assign(
+ LOCAL_ASSIGN;
+ VIF_ASSIGN;
+ STA_ASSIGN;
+ __entry->n_desc = n_desc;
+ ),
+
+ TP_printk(
+ LOCAL_PR_FMT VIF_PR_FMT STA_PR_FMT " %d descriptors",
+ LOCAL_PR_ARG, VIF_PR_ARG, STA_PR_ARG, __entry->n_desc
+ )
+);
+
DEFINE_EVENT(sta_event, drv_sta_statistics,
TP_PROTO(struct ieee80211_local *local,
struct ieee80211_sub_if_data *sdata,
diff --git a/net/mac80211/util.c b/net/mac80211/util.c
index e2b5b2b21c347..c97a3e00e2518 100644
--- a/net/mac80211/util.c
+++ b/net/mac80211/util.c
@@ -1744,6 +1744,8 @@ static void ieee80211_reconfig_stations(struct ieee80211_sub_if_data *sdata)
state < sta->sta_state; state++)
WARN_ON(drv_sta_state(local, sta->sdata, sta, state,
state + 1));
+
+ ieee80211_sta_scs_reconfig(sta);
}
}
diff --git a/net/mac80211/wme.c b/net/mac80211/wme.c
index 1601be5764145..9fcee1bc6ad59 100644
--- a/net/mac80211/wme.c
+++ b/net/mac80211/wme.c
@@ -174,6 +174,13 @@ u16 ieee80211_select_queue(struct ieee80211_sub_if_data *sdata,
goto downgrade;
}
+ /* A stream priority overrides the received one of a forwarded frame */
+ if (sta &&
+ (sdata->vif.type == NL80211_IFTYPE_AP ||
+ sdata->vif.type == NL80211_IFTYPE_AP_VLAN) &&
+ ieee80211_flow_classify(sta, skb))
+ goto downgrade;
+
/* use the data classifier to determine what 802.1d tag the
* data frame has */
qos_map = rcu_dereference(sdata->qos_map);
diff --git a/net/wireless/Makefile b/net/wireless/Makefile
index a77fd5ba63686..552f4557ca23d 100644
--- a/net/wireless/Makefile
+++ b/net/wireless/Makefile
@@ -8,7 +8,7 @@ obj-$(CONFIG_WEXT_PRIV) += wext-priv.o
cfg80211-y += core.o sysfs.o radiotap.o util.o reg.o scan.o nl80211.o
cfg80211-y += mlme.o ibss.o sme.o chan.o ethtool.o mesh.o ap.o trace.o ocb.o
-cfg80211-y += michael-mic.o pmsr.o
+cfg80211-y += michael-mic.o pmsr.o scs.o
cfg80211-$(CONFIG_OF) += of.o
cfg80211-$(CONFIG_CFG80211_DEBUGFS) += debugfs.o
cfg80211-$(CONFIG_CFG80211_WEXT) += wext-compat.o wext-sme.o
diff --git a/net/wireless/core.c b/net/wireless/core.c
index cde3ca85494de..d7b47cbbf5427 100644
--- a/net/wireless/core.c
+++ b/net/wireless/core.c
@@ -999,6 +999,16 @@ int wiphy_register(struct wiphy *wiphy)
rdev->ops->update_connect_params))
return -EINVAL;
+ if (WARN_ON(wiphy_ext_feature_isset(&rdev->wiphy,
+ NL80211_EXT_FEATURE_SCS) &&
+ !rdev->ops->set_scs))
+ return -EINVAL;
+
+ if (WARN_ON(wiphy_ext_feature_isset(&rdev->wiphy,
+ NL80211_EXT_FEATURE_MSCS) &&
+ !rdev->ops->set_mscs))
+ return -EINVAL;
+
if (wiphy->addresses)
memcpy(wiphy->perm_addr, wiphy->addresses[0].addr, ETH_ALEN);
diff --git a/net/wireless/core.h b/net/wireless/core.h
index 6138d207caf41..9e8c2c42d67a7 100644
--- a/net/wireless/core.h
+++ b/net/wireless/core.h
@@ -648,6 +648,14 @@ struct cfg80211_colocated_ap {
s8 psd_20;
};
+int cfg80211_tclas_count(const u8 *elems, size_t len);
+int cfg80211_parse_tclas(const u8 *elems, size_t len,
+ struct cfg80211_tclas *out, u8 n_tclas,
+ enum cfg80211_tclas_processing *processing);
+int cfg80211_parse_tclas_mask(const u8 *elems, size_t len, u32 *fields);
+bool cfg80211_scs_desc_valid(const struct cfg80211_scs_desc *desc);
+bool cfg80211_mscs_desc_valid(const struct cfg80211_mscs_desc *desc);
+
#if IS_ENABLED(CONFIG_CFG80211_KUNIT_TEST)
#define EXPORT_SYMBOL_IF_CFG80211_KUNIT(sym) EXPORT_SYMBOL_IF_KUNIT(sym)
#define VISIBLE_IF_CFG80211_KUNIT
diff --git a/net/wireless/nl80211.c b/net/wireless/nl80211.c
index 85c010793c53e..b7a16a478ad04 100644
--- a/net/wireless/nl80211.c
+++ b/net/wireless/nl80211.c
@@ -624,6 +624,31 @@ sar_policy[NL80211_SAR_ATTR_MAX + 1] = {
[NL80211_SAR_ATTR_SPECS] = NLA_POLICY_NESTED_ARRAY(sar_specs_policy),
};
+/*
+ * The element parsers validate the binary attributes, so that a malformed
+ * element is answered with a status code instead of a netlink error.
+ */
+static const struct nla_policy
+nl80211_scs_desc_policy[NL80211_SCS_DESC_ATTR_MAX + 1] = {
+ [NL80211_SCS_DESC_ATTR_ID] = NLA_POLICY_MIN(NLA_U8, 1),
+ [NL80211_SCS_DESC_ATTR_REQ_TYPE] =
+ NLA_POLICY_MAX(NLA_U8, NL80211_SCS_REQ_CHANGE),
+ [NL80211_SCS_DESC_ATTR_UP] = NLA_POLICY_MAX(NLA_U8, 7),
+ [NL80211_SCS_DESC_ATTR_TCLAS] = { .type = NLA_BINARY },
+ [NL80211_SCS_DESC_ATTR_QOS_CHAR] = { .type = NLA_BINARY },
+ [NL80211_SCS_DESC_ATTR_STATUS] = { .type = NLA_REJECT },
+};
+
+static const struct nla_policy
+nl80211_mscs_desc_policy[NL80211_MSCS_DESC_ATTR_MAX + 1] = {
+ [NL80211_MSCS_DESC_ATTR_REQ_TYPE] =
+ NLA_POLICY_MAX(NLA_U8, NL80211_SCS_REQ_CHANGE),
+ [NL80211_MSCS_DESC_ATTR_UP_BITMAP] = { .type = NLA_U8 },
+ [NL80211_MSCS_DESC_ATTR_UP_LIMIT] = NLA_POLICY_MAX(NLA_U8, 7),
+ [NL80211_MSCS_DESC_ATTR_STREAM_TIMEOUT] = { .type = NLA_U32 },
+ [NL80211_MSCS_DESC_ATTR_TCLAS_MASK] = { .type = NLA_BINARY },
+};
+
static const struct nla_policy
nl80211_mbssid_config_policy[NL80211_MBSSID_CONFIG_ATTR_MAX + 1] = {
[NL80211_MBSSID_CONFIG_ATTR_MAX_INTERFACES] = NLA_POLICY_MIN(NLA_U8, 2),
@@ -1098,6 +1123,10 @@ static const struct nla_policy nl80211_policy[NUM_NL80211_ATTR] = {
NLA_POLICY_FULL_RANGE(NLA_U32, &nl80211_punct_bitmap_range),
[NL80211_ATTR_STA_DUMP_LINK_STATS] = { .type = NLA_FLAG },
[NL80211_ATTR_FRAME_NO_STA] = { .type = NLA_FLAG },
+ [NL80211_ATTR_SCS_DESCRIPTORS] =
+ NLA_POLICY_NESTED_ARRAY(nl80211_scs_desc_policy),
+ [NL80211_ATTR_MSCS_DESCRIPTOR] =
+ NLA_POLICY_NESTED(nl80211_mscs_desc_policy),
};
/* policy for the key attributes */
@@ -18288,6 +18317,390 @@ static int nl80211_set_qos_map(struct sk_buff *skb,
return ret;
}
+#define NL80211_MAX_SCS_DESC 255
+
+/* A station can install the traffic description of its uplink stream */
+static int nl80211_scs_iftype_ok(struct net_device *dev)
+{
+ switch (dev->ieee80211_ptr->iftype) {
+ case NL80211_IFTYPE_AP:
+ case NL80211_IFTYPE_P2P_GO:
+ case NL80211_IFTYPE_STATION:
+ return 0;
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+/* Only an AP mirrors an uplink priority onto the downlink */
+static int nl80211_mscs_iftype_ok(struct net_device *dev)
+{
+ switch (dev->ieee80211_ptr->iftype) {
+ case NL80211_IFTYPE_AP:
+ case NL80211_IFTYPE_P2P_GO:
+ return 0;
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static u16 nl80211_scs_status(int err)
+{
+ switch (err) {
+ case -EOPNOTSUPP:
+ return WLAN_STATUS_REQUESTED_TCLAS_NOT_SUPPORTED_BY_AP;
+ case -ENOSPC:
+ return WLAN_STATUS_INSUFFICIENT_TCLAS_PROCESSING_RESOURCES;
+ default:
+ return WLAN_STATUS_REQUEST_DECLINED;
+ }
+}
+
+static void nl80211_free_scs_desc(struct cfg80211_scs_desc **desc,
+ unsigned int n)
+{
+ unsigned int i;
+
+ for (i = 0; i < n; i++)
+ kfree(desc[i]);
+
+ kfree(desc);
+}
+
+static struct cfg80211_scs_desc *
+nl80211_parse_scs_desc(struct nlattr *attr, struct cfg80211_scs_result *res,
+ struct genl_info *info)
+{
+ enum cfg80211_tclas_processing processing =
+ CFG80211_TCLAS_PROCESSING_ABSENT;
+ struct nlattr *tb[NL80211_SCS_DESC_ATTR_MAX + 1];
+ const struct ieee80211_qos_char_elem *qos_char = NULL;
+ struct nlattr *qos_attr, *tclas_attr;
+ struct cfg80211_scs_desc *desc;
+ bool wants_up = true;
+ int n_tclas = 0;
+ bool remove;
+ int err;
+
+ err = nla_parse_nested(tb, NL80211_SCS_DESC_ATTR_MAX, attr,
+ nl80211_scs_desc_policy, info->extack);
+ if (err)
+ return ERR_PTR(err);
+
+ if (!tb[NL80211_SCS_DESC_ATTR_ID] ||
+ !tb[NL80211_SCS_DESC_ATTR_REQ_TYPE]) {
+ GENL_SET_ERR_MSG(info,
+ "SCS descriptor without an ID or a request type");
+ return ERR_PTR(-EINVAL);
+ }
+
+ remove = nla_get_u8(tb[NL80211_SCS_DESC_ATTR_REQ_TYPE]) ==
+ NL80211_SCS_REQ_REMOVE;
+
+ /* A removal names only an SCSID, the elements are ignored */
+ qos_attr = remove ? NULL : tb[NL80211_SCS_DESC_ATTR_QOS_CHAR];
+ if (qos_attr && ieee80211_qos_char_size_ok(nla_data(qos_attr),
+ nla_len(qos_attr)))
+ qos_char = nla_data(qos_attr);
+
+ /* An uplink or direct link descriptor carries no user priority */
+ if (qos_char && ieee80211_qos_char_direction(qos_char) !=
+ IEEE80211_QOS_CHAR_DIR_DOWNLINK)
+ wants_up = false;
+
+ /* Decline only this descriptor, the rest of the request stands */
+ if (!remove && (!qos_attr || qos_char) &&
+ wants_up != !!tb[NL80211_SCS_DESC_ATTR_UP])
+ err = -EINVAL;
+
+ tclas_attr = remove ? NULL : tb[NL80211_SCS_DESC_ATTR_TCLAS];
+ if (!err && tclas_attr) {
+ n_tclas = cfg80211_tclas_count(nla_data(tclas_attr),
+ nla_len(tclas_attr));
+ if (n_tclas < 0) {
+ err = n_tclas;
+ n_tclas = 0;
+ }
+ }
+
+ desc = kzalloc(struct_size(desc, tclas, n_tclas), GFP_KERNEL);
+ if (!desc)
+ return ERR_PTR(-ENOMEM);
+
+ desc->id = nla_get_u8(tb[NL80211_SCS_DESC_ATTR_ID]);
+ desc->req_type = nla_get_u8(tb[NL80211_SCS_DESC_ATTR_REQ_TYPE]);
+ desc->up = nla_get_u8_default(tb[NL80211_SCS_DESC_ATTR_UP], 0);
+ desc->n_tclas = n_tclas;
+
+ if (!err && tclas_attr) {
+ err = cfg80211_parse_tclas(nla_data(tclas_attr),
+ nla_len(tclas_attr), desc->tclas,
+ n_tclas, &processing);
+ if (err) {
+ desc->n_tclas = 0;
+ processing = CFG80211_TCLAS_PROCESSING_ABSENT;
+ }
+ }
+
+ desc->tclas_processing = processing;
+
+ if (!err && qos_attr) {
+ if (!qos_char) {
+ NL_SET_ERR_MSG(info->extack,
+ "bad QoS Characteristics element");
+ err = -EINVAL;
+ } else {
+ desc->qos_char = qos_char;
+ desc->qos_char_len = nla_len(qos_attr);
+ }
+ }
+
+ if (err)
+ res->status = nl80211_scs_status(err);
+ else if (!cfg80211_scs_desc_valid(desc))
+ res->status = WLAN_STATUS_REQUEST_DECLINED;
+
+ return desc;
+}
+
+static int nl80211_set_scs_reply(struct genl_info *info,
+ struct cfg80211_scs_desc * const *desc,
+ const struct cfg80211_scs_result *res,
+ unsigned int n_desc)
+{
+ struct nlattr *list, *entry;
+ struct sk_buff *msg;
+ unsigned int i;
+ size_t size;
+ void *hdr;
+
+ size = nla_total_size(nla_total_size(sizeof(u8)) +
+ nla_total_size(sizeof(u16)));
+
+ msg = nlmsg_new(NLMSG_DEFAULT_SIZE + n_desc * size, GFP_KERNEL);
+ if (!msg)
+ return -ENOMEM;
+
+ hdr = nl80211hdr_put(msg, info->snd_portid, info->snd_seq, 0,
+ NL80211_CMD_SET_SCS);
+ if (!hdr)
+ goto nla_put_failure;
+
+ list = nla_nest_start(msg, NL80211_ATTR_SCS_DESCRIPTORS);
+ if (!list)
+ goto nla_put_failure;
+
+ for (i = 0; i < n_desc; i++) {
+ entry = nla_nest_start(msg, i + 1);
+ if (!entry)
+ goto nla_put_failure;
+
+ if (nla_put_u8(msg, NL80211_SCS_DESC_ATTR_ID, desc[i]->id) ||
+ nla_put_u16(msg, NL80211_SCS_DESC_ATTR_STATUS,
+ res[i].status))
+ goto nla_put_failure;
+
+ nla_nest_end(msg, entry);
+ }
+
+ nla_nest_end(msg, list);
+ genlmsg_end(msg, hdr);
+
+ return genlmsg_reply(msg, info);
+
+nla_put_failure:
+ nlmsg_free(msg);
+
+ return -ENOBUFS;
+}
+
+static int nl80211_set_scs(struct sk_buff *skb, struct genl_info *info)
+{
+ struct cfg80211_registered_device *rdev = info->user_ptr[0];
+ struct net_device *dev = info->user_ptr[1];
+ unsigned int i, n_desc = 0, n_apply = 0;
+ struct cfg80211_scs_result *res;
+ struct cfg80211_scs_desc **desc;
+ struct nlattr *nla;
+ int rem, err;
+
+ if (!wiphy_ext_feature_isset(&rdev->wiphy, NL80211_EXT_FEATURE_SCS))
+ return -EOPNOTSUPP;
+
+ if (!info->attrs[NL80211_ATTR_MAC] ||
+ !info->attrs[NL80211_ATTR_SCS_DESCRIPTORS])
+ return -EINVAL;
+
+ if (info->attrs[NL80211_ATTR_MLO_LINK_ID]) {
+ GENL_SET_ERR_MSG(info,
+ "SCS rules belong to the MLD, not to a link");
+ return -EINVAL;
+ }
+
+ err = nl80211_scs_iftype_ok(dev);
+ if (err)
+ return err;
+
+ nla_for_each_nested(nla, info->attrs[NL80211_ATTR_SCS_DESCRIPTORS], rem)
+ n_desc++;
+
+ if (!n_desc || n_desc > NL80211_MAX_SCS_DESC)
+ return -EINVAL;
+
+ desc = kcalloc(n_desc, sizeof(*desc), GFP_KERNEL);
+ if (!desc)
+ return -ENOMEM;
+
+ res = kcalloc(n_desc, sizeof(*res), GFP_KERNEL);
+ if (!res) {
+ kfree(desc);
+ return -ENOMEM;
+ }
+
+ i = 0;
+ nla_for_each_nested(nla, info->attrs[NL80211_ATTR_SCS_DESCRIPTORS],
+ rem) {
+ desc[i] = nl80211_parse_scs_desc(nla, &res[i], info);
+ if (IS_ERR(desc[i])) {
+ err = PTR_ERR(desc[i]);
+ desc[i] = NULL;
+ goto out;
+ }
+
+ i++;
+ }
+
+ /* Pass only the descriptors that were not declined to the driver */
+ for (i = 0; i < n_desc; i++) {
+ if (res[i].status != WLAN_STATUS_SUCCESS)
+ continue;
+
+ swap(desc[i], desc[n_apply]);
+ swap(res[i], res[n_apply]);
+ n_apply++;
+ }
+
+ if (n_apply) {
+ err = rdev_set_scs(rdev, dev,
+ nla_data(info->attrs[NL80211_ATTR_MAC]),
+ desc, res, n_apply);
+ if (err)
+ goto out;
+ }
+
+ err = nl80211_set_scs_reply(info, desc, res, n_desc);
+
+out:
+ kfree(res);
+ nl80211_free_scs_desc(desc, n_desc);
+
+ return err;
+}
+
+static int nl80211_send_status_code(struct genl_info *info, u32 cmd, u16 status)
+{
+ struct sk_buff *msg;
+ void *hdr;
+
+ msg = nlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL);
+ if (!msg)
+ return -ENOMEM;
+
+ hdr = nl80211hdr_put(msg, info->snd_portid, info->snd_seq, 0, cmd);
+ if (!hdr || nla_put_u16(msg, NL80211_ATTR_STATUS_CODE, status)) {
+ nlmsg_free(msg);
+ return -ENOBUFS;
+ }
+
+ genlmsg_end(msg, hdr);
+
+ return genlmsg_reply(msg, info);
+}
+
+static int nl80211_set_mscs(struct sk_buff *skb, struct genl_info *info)
+{
+ struct cfg80211_registered_device *rdev = info->user_ptr[0];
+ struct nlattr *tb[NL80211_MSCS_DESC_ATTR_MAX + 1];
+ struct net_device *dev = info->user_ptr[1];
+ struct cfg80211_mscs_desc desc = {};
+ u16 status = WLAN_STATUS_SUCCESS;
+ struct nlattr *mask;
+ bool remove;
+ int err;
+
+ if (!wiphy_ext_feature_isset(&rdev->wiphy, NL80211_EXT_FEATURE_MSCS))
+ return -EOPNOTSUPP;
+
+ if (!info->attrs[NL80211_ATTR_MAC] ||
+ !info->attrs[NL80211_ATTR_MSCS_DESCRIPTOR])
+ return -EINVAL;
+
+ if (info->attrs[NL80211_ATTR_MLO_LINK_ID]) {
+ GENL_SET_ERR_MSG(info,
+ "an MSCS belongs to the MLD, not to a link");
+ return -EINVAL;
+ }
+
+ err = nl80211_mscs_iftype_ok(dev);
+ if (err)
+ return err;
+
+ err = nla_parse_nested(tb, NL80211_MSCS_DESC_ATTR_MAX,
+ info->attrs[NL80211_ATTR_MSCS_DESCRIPTOR],
+ nl80211_mscs_desc_policy, info->extack);
+ if (err)
+ return err;
+
+ if (!tb[NL80211_MSCS_DESC_ATTR_REQ_TYPE]) {
+ GENL_SET_ERR_MSG(info,
+ "MSCS descriptor without a request type");
+ return -EINVAL;
+ }
+
+ desc.req_type = nla_get_u8(tb[NL80211_MSCS_DESC_ATTR_REQ_TYPE]);
+ remove = desc.req_type == NL80211_SCS_REQ_REMOVE;
+
+ if (remove == !!tb[NL80211_MSCS_DESC_ATTR_TCLAS_MASK]) {
+ GENL_SET_ERR_MSG(info,
+ "TCLAS Mask does not match the request type");
+ return -EINVAL;
+ }
+
+ desc.up_bitmap =
+ nla_get_u8_default(tb[NL80211_MSCS_DESC_ATTR_UP_BITMAP], 0);
+ desc.up_limit = nla_get_u8_default(tb[NL80211_MSCS_DESC_ATTR_UP_LIMIT],
+ 7);
+ desc.stream_timeout =
+ nla_get_u32_default(tb[NL80211_MSCS_DESC_ATTR_STREAM_TIMEOUT],
+ 0);
+
+ mask = tb[NL80211_MSCS_DESC_ATTR_TCLAS_MASK];
+ if (mask) {
+ err = cfg80211_parse_tclas_mask(nla_data(mask), nla_len(mask),
+ &desc.fields);
+ if (err) {
+ status = nl80211_scs_status(err);
+ goto reply;
+ }
+ }
+
+ if (!cfg80211_mscs_desc_valid(&desc)) {
+ status = WLAN_STATUS_REQUEST_DECLINED;
+ goto reply;
+ }
+
+ err = rdev_set_mscs(rdev, dev,
+ nla_data(info->attrs[NL80211_ATTR_MAC]), &desc);
+ if (err)
+ return err;
+
+ if (remove)
+ status = WLAN_STATUS_TCLAS_PROCESSING_TERMINATED;
+
+reply:
+ return nl80211_send_status_code(info, NL80211_CMD_SET_MSCS, status);
+}
+
static int nl80211_add_tx_ts(struct sk_buff *skb, struct genl_info *info)
{
struct cfg80211_registered_device *rdev = info->user_ptr[0];
@@ -20446,6 +20859,18 @@ static const struct genl_small_ops nl80211_small_ops[] = {
.flags = GENL_UNS_ADMIN_PERM,
.internal_flags = IFLAGS(NL80211_FLAG_NEED_NETDEV_UP),
},
+ {
+ .cmd = NL80211_CMD_SET_SCS,
+ .doit = nl80211_set_scs,
+ .flags = GENL_UNS_ADMIN_PERM,
+ .internal_flags = IFLAGS(NL80211_FLAG_NEED_NETDEV_UP),
+ },
+ {
+ .cmd = NL80211_CMD_SET_MSCS,
+ .doit = nl80211_set_mscs,
+ .flags = GENL_UNS_ADMIN_PERM,
+ .internal_flags = IFLAGS(NL80211_FLAG_NEED_NETDEV_UP),
+ },
{
.cmd = NL80211_CMD_ADD_TX_TS,
.validate = GENL_DONT_VALIDATE_STRICT | GENL_DONT_VALIDATE_DUMP,
diff --git a/net/wireless/rdev-ops.h b/net/wireless/rdev-ops.h
index adcfd0278da32..8d5c99be74137 100644
--- a/net/wireless/rdev-ops.h
+++ b/net/wireless/rdev-ops.h
@@ -1188,6 +1188,38 @@ static inline int rdev_set_qos_map(struct cfg80211_registered_device *rdev,
return ret;
}
+static inline int rdev_set_scs(struct cfg80211_registered_device *rdev,
+ struct net_device *dev, const u8 *peer,
+ struct cfg80211_scs_desc * const *desc,
+ struct cfg80211_scs_result *res, u8 n_desc)
+{
+ int ret = -EOPNOTSUPP;
+
+ if (rdev->ops->set_scs) {
+ trace_rdev_set_scs(&rdev->wiphy, dev, peer, n_desc);
+ ret = rdev->ops->set_scs(&rdev->wiphy, dev, peer, desc, res,
+ n_desc);
+ trace_rdev_return_int(&rdev->wiphy, ret);
+ }
+
+ return ret;
+}
+
+static inline int rdev_set_mscs(struct cfg80211_registered_device *rdev,
+ struct net_device *dev, const u8 *peer,
+ struct cfg80211_mscs_desc *desc)
+{
+ int ret = -EOPNOTSUPP;
+
+ if (rdev->ops->set_mscs) {
+ trace_rdev_set_mscs(&rdev->wiphy, dev, peer, desc);
+ ret = rdev->ops->set_mscs(&rdev->wiphy, dev, peer, desc);
+ trace_rdev_return_int(&rdev->wiphy, ret);
+ }
+
+ return ret;
+}
+
static inline int
rdev_set_ap_chanwidth(struct cfg80211_registered_device *rdev,
struct net_device *dev,
diff --git a/net/wireless/scs.c b/net/wireless/scs.c
new file mode 100644
index 0000000000000..22c0b9d6930e2
--- /dev/null
+++ b/net/wireless/scs.c
@@ -0,0 +1,982 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Stream classification service (SCS) and mirrored SCS (MSCS)
+ *
+ * Copyright (C) 2026 Felix Fietkau <nbd@nbd.name>
+ */
+#include <kunit/visibility.h>
+#include <linux/ieee80211.h>
+#include <linux/if_vlan.h>
+#include <linux/ip.h>
+#include <linux/ipv6.h>
+#include <linux/unaligned.h>
+#include <net/cfg80211.h>
+#include <net/dsfield.h>
+#include <net/ip.h>
+#include <net/ipv6.h>
+#include "core.h"
+
+enum cfg80211_tclas_type {
+ CFG80211_TCLAS_ETH = 0,
+ CFG80211_TCLAS_IP = 4,
+ CFG80211_TCLAS_VLAN = 5,
+ CFG80211_TCLAS_FILTER = 10,
+};
+
+struct tclas_fc {
+ u8 type;
+ u8 mask;
+} __packed;
+
+struct tclas_fc_eth {
+ struct tclas_fc hdr;
+ u8 sa[ETH_ALEN];
+ u8 da[ETH_ALEN];
+ __be16 ethertype;
+} __packed;
+
+struct tclas_fc_ip4 {
+ struct tclas_fc hdr;
+ u8 version;
+ __be32 src;
+ __be32 dst;
+ __be16 src_port;
+ __be16 dst_port;
+ u8 dscp;
+ u8 proto;
+ u8 reserved;
+} __packed;
+
+struct tclas_fc_ip6 {
+ struct tclas_fc hdr;
+ u8 version;
+ struct in6_addr src;
+ struct in6_addr dst;
+ __be16 src_port;
+ __be16 dst_port;
+ u8 dscp;
+ u8 proto;
+ u8 flow_label[3];
+} __packed;
+
+struct tclas_fc_vlan {
+ struct tclas_fc hdr;
+ u8 pcp;
+ u8 dei;
+ __be16 vid;
+} __packed;
+
+/* Hashed and compared as a byte string, so it must contain no padding */
+static_assert(sizeof(struct cfg80211_flow_key) ==
+ 2 * sizeof(struct in6_addr) + sizeof(__be32) + 2 * ETH_ALEN +
+ 4 * sizeof(__be16) + 4);
+
+#define FLOW_F(name) BIT(CFG80211_FLOW_F_##name)
+
+#define FLOW_F_VLAN (FLOW_F(VLAN_PCP) | FLOW_F(VLAN_DEI) | FLOW_F(VLAN_VID))
+
+/* Reserved classifier mask bits above the array length are ignored */
+static const u8 tclas_map_eth[] = {
+ CFG80211_FLOW_F_ETH_SA,
+ CFG80211_FLOW_F_ETH_DA,
+ CFG80211_FLOW_F_ETH_TYPE,
+};
+
+/* Shared by both IP versions, only IPv6 has the flow label */
+static const u8 tclas_map_ip[] = {
+ CFG80211_FLOW_F_IP_VERSION,
+ CFG80211_FLOW_F_IP_SRC,
+ CFG80211_FLOW_F_IP_DST,
+ CFG80211_FLOW_F_SRC_PORT,
+ CFG80211_FLOW_F_DST_PORT,
+ CFG80211_FLOW_F_DSCP,
+ CFG80211_FLOW_F_PROTO,
+ CFG80211_FLOW_F_FLOW_LABEL,
+};
+
+static const u8 tclas_map_vlan[] = {
+ CFG80211_FLOW_F_VLAN_PCP,
+ CFG80211_FLOW_F_VLAN_DEI,
+ CFG80211_FLOW_F_VLAN_VID,
+};
+
+static u32 tclas_mask_fields(u8 type, u8 version, u8 mask)
+{
+ size_t n = ARRAY_SIZE(tclas_map_ip);
+ const u8 *map = tclas_map_ip;
+ unsigned int i;
+ u32 fields = 0;
+
+ switch (type) {
+ case CFG80211_TCLAS_ETH:
+ map = tclas_map_eth;
+ n = ARRAY_SIZE(tclas_map_eth);
+ break;
+ case CFG80211_TCLAS_VLAN:
+ map = tclas_map_vlan;
+ n = ARRAY_SIZE(tclas_map_vlan);
+ break;
+ default:
+ n -= version == 4;
+ break;
+ }
+
+ for (i = 0; i < n; i++)
+ if (mask & BIT(i))
+ fields |= BIT(map[i]);
+
+ return fields;
+}
+
+static void flow_key_select(const struct cfg80211_flow_key *src, u32 fields,
+ struct cfg80211_flow_key *dst)
+{
+ memset(dst, 0, sizeof(*dst));
+
+ if (fields & FLOW_F(ETH_SA))
+ memcpy(dst->sa, src->sa, ETH_ALEN);
+ if (fields & FLOW_F(ETH_DA))
+ memcpy(dst->da, src->da, ETH_ALEN);
+ if (fields & FLOW_F(ETH_TYPE))
+ dst->eth_type = src->eth_type;
+
+ if (fields & FLOW_F_VLAN) {
+ u16 tci = 0;
+
+ if (fields & FLOW_F(VLAN_PCP))
+ tci |= VLAN_PRIO_MASK;
+ if (fields & FLOW_F(VLAN_DEI))
+ tci |= VLAN_CFI_MASK;
+ if (fields & FLOW_F(VLAN_VID))
+ tci |= VLAN_VID_MASK;
+
+ dst->vlan_tci = src->vlan_tci & htons(tci);
+ }
+
+ if (fields & FLOW_F(IP_VERSION))
+ dst->ip_version = src->ip_version;
+ if (fields & FLOW_F(IP_SRC))
+ dst->src = src->src;
+ if (fields & FLOW_F(IP_DST))
+ dst->dst = src->dst;
+ if (fields & FLOW_F(SRC_PORT))
+ dst->src_port = src->src_port;
+ if (fields & FLOW_F(DST_PORT))
+ dst->dst_port = src->dst_port;
+
+ if (fields & FLOW_F(DSCP))
+ dst->dscp = src->dscp;
+ if (fields & FLOW_F(PROTO))
+ dst->proto = src->proto;
+ if (fields & FLOW_F(FLOW_LABEL))
+ dst->flow_label = src->flow_label;
+}
+
+static int tclas_parse_eth(struct cfg80211_flow_key *key, const u8 *data,
+ size_t len)
+{
+ const struct tclas_fc_eth *fc = (const void *)data;
+
+ if (len != sizeof(*fc))
+ return -EINVAL;
+
+ memcpy(key->sa, fc->sa, ETH_ALEN);
+ memcpy(key->da, fc->da, ETH_ALEN);
+ key->eth_type = fc->ethertype;
+
+ return 0;
+}
+
+static int tclas_parse_ip4(struct cfg80211_flow_key *key,
+ const struct tclas_fc_ip4 *fc, bool values)
+{
+ if (values && fc->version != 4)
+ return -EINVAL;
+
+ key->ip_version = 4;
+ memcpy(&key->src, &fc->src, sizeof(fc->src));
+ memcpy(&key->dst, &fc->dst, sizeof(fc->dst));
+ key->src_port = fc->src_port;
+ key->dst_port = fc->dst_port;
+ key->dscp = fc->dscp & 0x3f;
+ key->proto = fc->proto;
+
+ return 0;
+}
+
+static int tclas_parse_ip6(struct cfg80211_flow_key *key,
+ const struct tclas_fc_ip6 *fc, bool values)
+{
+ if (values && fc->version != 6)
+ return -EINVAL;
+
+ key->ip_version = 6;
+ memcpy(&key->src, &fc->src, sizeof(fc->src));
+ memcpy(&key->dst, &fc->dst, sizeof(fc->dst));
+ key->src_port = fc->src_port;
+ key->dst_port = fc->dst_port;
+ key->dscp = fc->dscp & 0x3f;
+ key->proto = fc->proto;
+ /* Same encoding as ip6_flowlabel(), so the two compare equal */
+ key->flow_label =
+ cpu_to_be32(get_unaligned_be24(fc->flow_label) & 0xfffff);
+
+ return 0;
+}
+
+/* The Version subfield is reserved in a mask, so the length selects v4/v6 */
+static int tclas_parse_ip(struct cfg80211_flow_key *key, const u8 *data,
+ size_t len, bool values)
+{
+ if (len == sizeof(struct tclas_fc_ip4))
+ return tclas_parse_ip4(key, (const void *)data, values);
+
+ if (len == sizeof(struct tclas_fc_ip6))
+ return tclas_parse_ip6(key, (const void *)data, values);
+
+ return -EINVAL;
+}
+
+static int tclas_parse_vlan(struct cfg80211_flow_key *key, const u8 *data,
+ size_t len)
+{
+ const struct tclas_fc_vlan *fc = (const void *)data;
+ u16 tci;
+
+ if (len != sizeof(*fc))
+ return -EINVAL;
+
+ if (fc->pcp > 7 || fc->dei > 1 || be16_to_cpu(fc->vid) > 0xfff)
+ return -EINVAL;
+
+ tci = fc->pcp << 13;
+ tci |= fc->dei << 12;
+ tci |= be16_to_cpu(fc->vid);
+ key->vlan_tci = cpu_to_be16(tci);
+
+ return 0;
+}
+
+/* The octets after the header are the value and the mask, of equal length */
+static int tclas_parse_filter(struct cfg80211_tclas *t, const u8 *data,
+ size_t len)
+{
+ struct cfg80211_tclas_filter *f = &t->filter;
+
+ if (len < 3 || (len - 3) % 2)
+ return -EINVAL;
+
+ f->len = (len - 3) / 2;
+ if (!f->len || f->len > CFG80211_TCLAS_FILTER_LEN)
+ return -EOPNOTSUPP;
+
+ f->instance = data[1];
+ f->protocol = data[2];
+ memcpy(f->value, data + 3, f->len);
+ memcpy(f->mask, data + 3 + f->len, f->len);
+
+ return 0;
+}
+
+/* @values is false for a TCLAS Mask element, which carries no values */
+static int cfg80211_parse_frame_classifier(struct cfg80211_tclas *t,
+ const u8 *data, size_t len,
+ bool values)
+{
+ const struct tclas_fc *fc = (const void *)data;
+ struct cfg80211_flow_key raw = {};
+ int ret;
+
+ if (len < sizeof(*fc))
+ return -EINVAL;
+
+ memset(t, 0, sizeof(*t));
+ t->type = fc->type;
+
+ switch (t->type) {
+ case CFG80211_TCLAS_ETH:
+ ret = tclas_parse_eth(&raw, data, len);
+ break;
+ case CFG80211_TCLAS_IP:
+ ret = tclas_parse_ip(&raw, data, len, values);
+ break;
+ case CFG80211_TCLAS_VLAN:
+ ret = tclas_parse_vlan(&raw, data, len);
+ break;
+ case CFG80211_TCLAS_FILTER:
+ /* A mask element selects fields, which a filter does not */
+ if (!values)
+ return -EOPNOTSUPP;
+
+ return tclas_parse_filter(t, data, len);
+ default:
+ return -EOPNOTSUPP;
+ }
+
+ if (ret)
+ return ret;
+
+ t->fields = tclas_mask_fields(t->type, raw.ip_version, fc->mask);
+
+ /* A mask that selects nothing would match every MSDU */
+ if (!t->fields)
+ return -EINVAL;
+
+ /* Always match on the IP version, which the element length selects */
+ if (t->type == CFG80211_TCLAS_IP)
+ t->fields |= FLOW_F(IP_VERSION);
+
+ flow_key_select(&raw, t->fields, &t->key);
+
+ return 0;
+}
+
+/**
+ * cfg80211_tclas_count - count the TCLAS elements of a chain
+ *
+ * @elems: element chain, as it arrived over the air
+ * @len: length of @elems
+ *
+ * Sizes the array for cfg80211_parse_tclas(). Does not validate the
+ * elements.
+ *
+ * Return: the number of TCLAS elements, or -ENOSPC for more than a descriptor
+ * can hold.
+ */
+int cfg80211_tclas_count(const u8 *elems, size_t len)
+{
+ const struct element *elem;
+ unsigned int n = 0;
+
+ for_each_element_id(elem, WLAN_EID_TCLAS, elems, len)
+ n++;
+
+ if (n > U8_MAX)
+ return -ENOSPC;
+
+ return n;
+}
+EXPORT_SYMBOL_IF_CFG80211_KUNIT(cfg80211_tclas_count);
+
+/**
+ * cfg80211_parse_tclas - parse a chain of TCLAS elements
+ *
+ * @elems: element chain, as it arrived over the air
+ * @len: length of @elems
+ * @out: array that receives the parsed elements
+ * @n_tclas: number of entries in @out, from cfg80211_tclas_count()
+ * @processing: receives how the elements relate, or
+ * %CFG80211_TCLAS_PROCESSING_ABSENT when the chain carries no TCLAS
+ * Processing element
+ *
+ * The chain holds TCLAS elements and at most one TCLAS Processing element.
+ *
+ * Return: 0 on success, -EINVAL for a malformed chain, -EOPNOTSUPP for a
+ * classifier the kernel cannot evaluate, -ENOSPC for a chain that no
+ * longer fits @out.
+ */
+int cfg80211_parse_tclas(const u8 *elems, size_t len,
+ struct cfg80211_tclas *out, u8 n_tclas,
+ enum cfg80211_tclas_processing *processing)
+{
+ const struct element *elem;
+ unsigned int n = 0;
+ int ret;
+
+ *processing = CFG80211_TCLAS_PROCESSING_ABSENT;
+
+ for_each_element(elem, elems, len) {
+ switch (elem->id) {
+ case WLAN_EID_TCLAS:
+ if (n == n_tclas)
+ return -ENOSPC;
+
+ if (elem->datalen < 1)
+ return -EINVAL;
+
+ /*
+ * Any other value matches on the UP of the MSDU,
+ * which is not known yet at classification time.
+ */
+ if (elem->data[0] != 255)
+ return -EOPNOTSUPP;
+
+ ret = cfg80211_parse_frame_classifier(&out[n],
+ elem->data + 1,
+ elem->datalen - 1,
+ true);
+ if (ret)
+ return ret;
+
+ n++;
+ break;
+ case WLAN_EID_TCLAS_PROCESSING:
+ if (elem->datalen != 1 ||
+ *processing != CFG80211_TCLAS_PROCESSING_ABSENT ||
+ elem->data[0] > CFG80211_TCLAS_PROCESSING_DEFAULT)
+ return -EINVAL;
+
+ *processing = elem->data[0];
+ break;
+ default:
+ return -EINVAL;
+ }
+ }
+
+ if (!for_each_element_completed(elem, elems, len))
+ return -EINVAL;
+
+ return 0;
+}
+EXPORT_SYMBOL_IF_CFG80211_KUNIT(cfg80211_parse_tclas);
+
+/**
+ * cfg80211_parse_tclas_mask - parse a chain of TCLAS Mask elements
+ *
+ * @elems: element chain, as it arrived over the air
+ * @len: length of @elems
+ * @fields: receives the union of the selected classifier parameters, as a
+ * bitmap of &enum cfg80211_flow_field
+ *
+ * A TCLAS Mask element carries no values, only the field selection.
+ *
+ * Return: 0 on success, -EINVAL for a malformed chain, -EOPNOTSUPP for a
+ * classifier type the kernel cannot evaluate.
+ */
+int cfg80211_parse_tclas_mask(const u8 *elems, size_t len, u32 *fields)
+{
+ const struct element *elem;
+ struct cfg80211_tclas t;
+ int ret;
+
+ *fields = 0;
+
+ for_each_element(elem, elems, len) {
+ if (elem->id != WLAN_EID_EXTENSION || elem->datalen < 1 ||
+ elem->data[0] != WLAN_EID_EXT_TCLAS_MASK)
+ return -EINVAL;
+
+ ret = cfg80211_parse_frame_classifier(&t, elem->data + 1,
+ elem->datalen - 1, false);
+ if (ret)
+ return ret;
+
+ *fields |= t.fields;
+ }
+
+ if (!for_each_element_completed(elem, elems, len))
+ return -EINVAL;
+
+ return 0;
+}
+EXPORT_SYMBOL_IF_CFG80211_KUNIT(cfg80211_parse_tclas_mask);
+
+#define FLOW_F_PORTS (FLOW_F(SRC_PORT) | FLOW_F(DST_PORT))
+
+static void cfg80211_flow_parse_ports(struct sk_buff *skb,
+ unsigned int offset, int proto,
+ struct cfg80211_flow_info *info)
+{
+ __be16 ports[2], *p;
+
+ switch (proto) {
+ case IPPROTO_TCP:
+ case IPPROTO_UDP:
+ case IPPROTO_SCTP:
+ break;
+ default:
+ return;
+ }
+
+ p = skb_header_pointer(skb, offset, sizeof(ports), ports);
+ if (!p)
+ return;
+
+ info->key.src_port = p[0];
+ info->key.dst_port = p[1];
+ info->present |= FLOW_F_PORTS;
+}
+
+static void cfg80211_flow_parse_ipv4(struct sk_buff *skb, unsigned int offset,
+ struct cfg80211_flow_info *info)
+{
+ struct iphdr iph_buf, *iph;
+
+ iph = skb_header_pointer(skb, offset, sizeof(*iph), &iph_buf);
+ if (!iph || iph->version != 4 || iph->ihl < 5)
+ return;
+
+ info->key.ip_version = 4;
+ info->key.dscp = ipv4_get_dsfield(iph) >> 2;
+ info->key.proto = iph->protocol;
+ memcpy(&info->key.src, &iph->saddr, sizeof(iph->saddr));
+ memcpy(&info->key.dst, &iph->daddr, sizeof(iph->daddr));
+ info->present |= FLOW_F(IP_VERSION) | FLOW_F(IP_SRC) | FLOW_F(IP_DST) |
+ FLOW_F(DSCP) | FLOW_F(PROTO);
+
+ if (iph->frag_off & htons(IP_OFFSET))
+ return;
+
+ cfg80211_flow_parse_ports(skb, offset + iph->ihl * 4, iph->protocol,
+ info);
+}
+
+static void cfg80211_flow_parse_ipv6(struct sk_buff *skb, unsigned int offset,
+ struct cfg80211_flow_info *info)
+{
+ struct ipv6hdr ip6h_buf, *ip6h;
+ __be16 frag_off = 0;
+ int l4_off;
+ u8 nexthdr;
+
+ ip6h = skb_header_pointer(skb, offset, sizeof(*ip6h), &ip6h_buf);
+ if (!ip6h || ip6h->version != 6)
+ return;
+
+ info->key.ip_version = 6;
+ info->key.dscp = ipv6_get_dsfield(ip6h) >> 2;
+ info->key.flow_label = ip6_flowlabel(ip6h);
+ info->key.src = ip6h->saddr;
+ info->key.dst = ip6h->daddr;
+
+ /* The classifier matches Next Header, not the layer 4 protocol */
+ info->key.proto = ip6h->nexthdr;
+ info->present |= FLOW_F(IP_VERSION) | FLOW_F(IP_SRC) | FLOW_F(IP_DST) |
+ FLOW_F(DSCP) | FLOW_F(FLOW_LABEL) | FLOW_F(PROTO);
+
+ nexthdr = ip6h->nexthdr;
+ l4_off = ipv6_skip_exthdr(skb, offset + sizeof(*ip6h), &nexthdr,
+ &frag_off);
+ if (l4_off < 0 || frag_off & htons(IP6_OFFSET))
+ return;
+
+ cfg80211_flow_parse_ports(skb, l4_off, nexthdr, info);
+}
+
+/**
+ * cfg80211_flow_parse - read the classifier parameters of an MSDU
+ *
+ * @skb: the frame, in IEEE 802.3 format
+ * @info: receives the parameters
+ *
+ * Parses from skb->data, since a frame forwarded between two stations
+ * arrives with skb->protocol set to ETH_P_802_3 and no network header.
+ *
+ * Return: %true when @info describes the frame.
+ */
+bool cfg80211_flow_parse(struct sk_buff *skb, struct cfg80211_flow_info *info)
+{
+ struct ethhdr eth_buf, *eth;
+ unsigned int offset = ETH_HLEN;
+ __be16 proto;
+
+ memset(info, 0, sizeof(*info));
+
+ eth = skb_header_pointer(skb, 0, sizeof(*eth), ð_buf);
+ if (!eth)
+ return false;
+
+ ether_addr_copy(info->key.sa, eth->h_source);
+ ether_addr_copy(info->key.da, eth->h_dest);
+ info->present = FLOW_F(ETH_SA) | FLOW_F(ETH_DA);
+ proto = eth->h_proto;
+
+ if (skb_vlan_tag_present(skb)) {
+ info->key.vlan_tci = htons(skb_vlan_tag_get(skb));
+ info->present |= FLOW_F_VLAN;
+ } else if (eth_type_vlan(proto)) {
+ struct vlan_hdr vhdr_buf, *vhdr;
+
+ vhdr = skb_header_pointer(skb, offset, sizeof(*vhdr),
+ &vhdr_buf);
+ if (!vhdr)
+ return false;
+
+ info->key.vlan_tci = vhdr->h_vlan_TCI;
+ info->present |= FLOW_F_VLAN;
+ proto = vhdr->h_vlan_encapsulated_proto;
+ offset += sizeof(*vhdr);
+ }
+
+ /* Use the type behind the VLAN tag, whether it was stripped or not */
+ info->key.eth_type = proto;
+ info->present |= FLOW_F(ETH_TYPE);
+
+ info->skb = skb;
+ info->l3_off = offset;
+
+ switch (proto) {
+ case htons(ETH_P_IP):
+ cfg80211_flow_parse_ipv4(skb, offset, info);
+ break;
+ case htons(ETH_P_IPV6):
+ cfg80211_flow_parse_ipv6(skb, offset, info);
+ break;
+ }
+
+ return true;
+}
+EXPORT_SYMBOL(cfg80211_flow_parse);
+
+static void flow_key_swap(struct cfg80211_flow_key *k)
+{
+ u8 addr[ETH_ALEN];
+
+ memcpy(addr, k->sa, ETH_ALEN);
+ memcpy(k->sa, k->da, ETH_ALEN);
+ memcpy(k->da, addr, ETH_ALEN);
+
+ swap(k->src, k->dst);
+ swap(k->src_port, k->dst_port);
+}
+
+static u32 cfg80211_flow_fields_mirror(u32 fields)
+{
+ static const u8 pairs[][2] = {
+ { CFG80211_FLOW_F_ETH_SA, CFG80211_FLOW_F_ETH_DA },
+ { CFG80211_FLOW_F_IP_SRC, CFG80211_FLOW_F_IP_DST },
+ { CFG80211_FLOW_F_SRC_PORT, CFG80211_FLOW_F_DST_PORT },
+ };
+ unsigned int i;
+ u32 out = fields;
+
+ for (i = 0; i < ARRAY_SIZE(pairs); i++) {
+ u32 lo = BIT(pairs[i][0]), hi = BIT(pairs[i][1]);
+
+ out &= ~(lo | hi);
+ if (fields & lo)
+ out |= hi;
+ if (fields & hi)
+ out |= lo;
+ }
+
+ return out;
+}
+
+/**
+ * cfg80211_flow_key_build - build an MSCS lookup key
+ *
+ * @info: parsed frame, from cfg80211_flow_parse()
+ * @fields: classifier parameters of the MSCS, a bitmap of
+ * &enum cfg80211_flow_field
+ * @dir: %CFG80211_FLOW_MIRRORED when @info describes an uplink frame and the
+ * key must describe the downlink direction
+ * @key: receives the key
+ *
+ * Everything that @fields does not select stays zero, so the key is hashed and
+ * compared as a byte string.
+ *
+ * Return: %false when @info lacks a parameter that @fields selects, in which
+ * case the MSDU is not classified at all.
+ */
+bool cfg80211_flow_key_build(const struct cfg80211_flow_info *info, u32 fields,
+ enum cfg80211_flow_dir dir,
+ struct cfg80211_flow_key *key)
+{
+ struct cfg80211_flow_key mirrored;
+
+ if (dir == CFG80211_FLOW_AS_IS) {
+ if (fields & ~info->present)
+ return false;
+
+ flow_key_select(&info->key, fields, key);
+
+ return true;
+ }
+
+ if (cfg80211_flow_fields_mirror(fields) & ~info->present)
+ return false;
+
+ mirrored = info->key;
+ flow_key_swap(&mirrored);
+ flow_key_select(&mirrored, fields, key);
+
+ return true;
+}
+EXPORT_SYMBOL(cfg80211_flow_key_build);
+
+/* Find the payload behind the @instance-th occurrence of the protocol */
+static bool tclas_filter_offset(const struct cfg80211_flow_info *info,
+ const struct cfg80211_tclas_filter *f,
+ unsigned int *off)
+{
+ struct ipv6hdr ip6h_buf, *ip6h;
+ unsigned int pos = info->l3_off;
+ unsigned int hdrs = 0;
+ u8 instance = 0;
+ u8 nexthdr;
+
+ if (info->key.ip_version == 4) {
+ struct iphdr iph_buf, *iph;
+
+ iph = skb_header_pointer(info->skb, pos, sizeof(*iph),
+ &iph_buf);
+ if (!iph || iph->protocol != f->protocol || f->instance)
+ return false;
+
+ *off = pos + iph->ihl * 4;
+
+ return true;
+ }
+
+ if (info->key.ip_version != 6)
+ return false;
+
+ ip6h = skb_header_pointer(info->skb, pos, sizeof(*ip6h), &ip6h_buf);
+ if (!ip6h)
+ return false;
+
+ nexthdr = ip6h->nexthdr;
+ pos += sizeof(*ip6h);
+
+ while (hdrs++ < 8) {
+ struct ipv6_opt_hdr hdr_buf, *hdr;
+
+ if (nexthdr == f->protocol && instance++ == f->instance) {
+ *off = pos;
+
+ return true;
+ }
+
+ if (!ipv6_ext_hdr(nexthdr) || nexthdr == NEXTHDR_NONE)
+ return false;
+
+ hdr = skb_header_pointer(info->skb, pos, sizeof(*hdr),
+ &hdr_buf);
+ if (!hdr)
+ return false;
+
+ if (nexthdr == NEXTHDR_FRAGMENT)
+ pos += 8;
+ else if (nexthdr == NEXTHDR_AUTH)
+ pos += ipv6_authlen(hdr);
+ else
+ pos += ipv6_optlen(hdr);
+
+ nexthdr = hdr->nexthdr;
+ }
+
+ return false;
+}
+
+static bool tclas_filter_match(const struct cfg80211_tclas *t,
+ const struct cfg80211_flow_info *info)
+{
+ const struct cfg80211_tclas_filter *f = &t->filter;
+ u8 buf[CFG80211_TCLAS_FILTER_LEN], *p;
+ unsigned int i, off;
+
+ if (!info->skb)
+ return false;
+
+ if (!tclas_filter_offset(info, f, &off))
+ return false;
+
+ p = skb_header_pointer(info->skb, off, f->len, buf);
+ if (!p)
+ return false;
+
+ for (i = 0; i < f->len; i++)
+ if ((p[i] ^ f->value[i]) & f->mask[i])
+ return false;
+
+ return true;
+}
+
+static bool tclas_match(const struct cfg80211_tclas *t,
+ const struct cfg80211_flow_info *info)
+{
+ struct cfg80211_flow_key key;
+
+ if (t->type == CFG80211_TCLAS_FILTER)
+ return tclas_filter_match(t, info);
+
+ if (!cfg80211_flow_key_build(info, t->fields, CFG80211_FLOW_AS_IS,
+ &key))
+ return false;
+
+ return !memcmp(&key, &t->key, sizeof(key));
+}
+
+/* Number of classifier parameters required for a match, see 11.25.2 */
+static u16 tclas_param_count(const struct cfg80211_tclas *t)
+{
+ if (t->type == CFG80211_TCLAS_FILTER)
+ return 1;
+
+ if (t->type == CFG80211_TCLAS_VLAN)
+ return 3;
+
+ return hweight32(t->fields & ~FLOW_F(IP_VERSION));
+}
+
+static bool scs_desc_match(const struct cfg80211_scs_desc *desc,
+ const struct cfg80211_flow_info *info)
+{
+ unsigned int i;
+ bool any = false;
+
+ for (i = 0; i < desc->n_tclas; i++) {
+ if (tclas_match(&desc->tclas[i], info)) {
+ any = true;
+ continue;
+ }
+
+ if (desc->tclas_processing == CFG80211_TCLAS_PROCESSING_ALL)
+ return false;
+ }
+
+ return any;
+}
+
+static u16 scs_desc_param_count(const struct cfg80211_scs_desc *desc)
+{
+ unsigned int i;
+ u16 count;
+
+ if (desc->tclas_processing != CFG80211_TCLAS_PROCESSING_ALL) {
+ count = U16_MAX;
+ for (i = 0; i < desc->n_tclas; i++)
+ count = min(count, tclas_param_count(&desc->tclas[i]));
+
+ return count == U16_MAX ? 0 : count;
+ }
+
+ count = 0;
+ for (i = 0; i < desc->n_tclas; i++)
+ count += tclas_param_count(&desc->tclas[i]);
+
+ return count;
+}
+
+/**
+ * cfg80211_scs_evaluate - pick the SCS descriptor that matches a flow
+ *
+ * @desc: the active descriptors of one peer
+ * @n_desc: number of entries in @desc
+ * @info: the flow, from cfg80211_flow_parse()
+ * @verdict: receives the result
+ *
+ * When several descriptors match, the one with the most classifier parameters
+ * wins. An equal count keeps the first.
+ */
+void cfg80211_scs_evaluate(struct cfg80211_scs_desc * const *desc, u8 n_desc,
+ const struct cfg80211_flow_info *info,
+ struct cfg80211_scs_verdict *verdict)
+{
+ unsigned int i;
+ int best = -1;
+
+ memset(verdict, 0, sizeof(*verdict));
+
+ for (i = 0; i < n_desc; i++) {
+ u16 count;
+
+ if (!scs_desc_match(desc[i], info))
+ continue;
+
+ count = scs_desc_param_count(desc[i]);
+ if (count <= best)
+ continue;
+
+ best = count;
+ verdict->match = true;
+ verdict->scsid = desc[i]->id;
+ verdict->up = desc[i]->up;
+ }
+}
+EXPORT_SYMBOL(cfg80211_scs_evaluate);
+
+/* Two elements of a match-all descriptor can require conflicting values */
+static bool tclas_conflict(const struct cfg80211_tclas *a,
+ const struct cfg80211_tclas *b)
+{
+ struct cfg80211_flow_key ka, kb;
+ u32 common = a->fields & b->fields;
+
+ if (!common)
+ return false;
+
+ flow_key_select(&a->key, common, &ka);
+ flow_key_select(&b->key, common, &kb);
+
+ return memcmp(&ka, &kb, sizeof(ka));
+}
+
+/**
+ * cfg80211_scs_desc_valid - validate one SCS descriptor
+ *
+ * @desc: the descriptor, already parsed
+ *
+ * Checks the constraints between the request type, the classifier and the
+ * traffic description. cfg80211_parse_tclas() validates each element.
+ *
+ * Return: whether the descriptor may be installed. The caller reports a
+ * refusal as a status code, not as an error.
+ */
+bool cfg80211_scs_desc_valid(const struct cfg80211_scs_desc *desc)
+{
+ bool want_tclas = true;
+ unsigned int i, j;
+
+ if (desc->req_type == NL80211_SCS_REQ_REMOVE)
+ return true;
+
+ /* Only a downlink stream is classified by the AP */
+ if (desc->qos_char &&
+ ieee80211_qos_char_direction(desc->qos_char) !=
+ IEEE80211_QOS_CHAR_DIR_DOWNLINK)
+ want_tclas = false;
+
+ if (want_tclas != !!desc->n_tclas)
+ return false;
+
+ if (!want_tclas)
+ return desc->tclas_processing ==
+ CFG80211_TCLAS_PROCESSING_ABSENT;
+
+ switch (desc->tclas_processing) {
+ case CFG80211_TCLAS_PROCESSING_ALL:
+ case CFG80211_TCLAS_PROCESSING_ANY:
+ if (desc->n_tclas < 2)
+ return false;
+ break;
+ case CFG80211_TCLAS_PROCESSING_DEFAULT:
+ return false;
+ case CFG80211_TCLAS_PROCESSING_ABSENT:
+ if (desc->n_tclas > 1)
+ return false;
+ break;
+ default:
+ return false;
+ }
+
+ if (desc->tclas_processing != CFG80211_TCLAS_PROCESSING_ALL)
+ return true;
+
+ for (i = 0; i < desc->n_tclas; i++)
+ for (j = i + 1; j < desc->n_tclas; j++)
+ if (tclas_conflict(&desc->tclas[i], &desc->tclas[j]))
+ return false;
+
+ return true;
+}
+EXPORT_SYMBOL_IF_CFG80211_KUNIT(cfg80211_scs_desc_valid);
+
+/**
+ * cfg80211_mscs_desc_valid - validate an MSCS descriptor
+ *
+ * @desc: the descriptor, already parsed
+ *
+ * Return: whether the descriptor may be installed.
+ */
+bool cfg80211_mscs_desc_valid(const struct cfg80211_mscs_desc *desc)
+{
+ bool remove = desc->req_type == NL80211_SCS_REQ_REMOVE;
+
+ if (remove == !!desc->fields)
+ return false;
+
+ /* A zero timeout would expire every entry at the next collection */
+ return remove || desc->stream_timeout;
+}
diff --git a/net/wireless/tests/Makefile b/net/wireless/tests/Makefile
index c364e63b508eb..a4b7f2e206fc2 100644
--- a/net/wireless/tests/Makefile
+++ b/net/wireless/tests/Makefile
@@ -1,3 +1,3 @@
-cfg80211-tests-y += module.o fragmentation.o scan.o util.o chan.o
+cfg80211-tests-y += module.o fragmentation.o scan.o util.o chan.o scs.o
obj-$(CONFIG_CFG80211_KUNIT_TEST) += cfg80211-tests.o
diff --git a/net/wireless/tests/scs.c b/net/wireless/tests/scs.c
new file mode 100644
index 0000000000000..d3636bd459644
--- /dev/null
+++ b/net/wireless/tests/scs.c
@@ -0,0 +1,488 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * KUnit tests for the SCS and MSCS classifier
+ *
+ * Copyright (C) 2026 Felix Fietkau <nbd@nbd.name>
+ */
+#include <kunit/skbuff.h>
+#include <kunit/test.h>
+#include <linux/ieee80211.h>
+#include <linux/ip.h>
+#include <net/cfg80211.h>
+#include "../core.h"
+
+static const u8 test_da[ETH_ALEN] = { 0x02, 0x00, 0x00, 0x00, 0x00, 0x02 };
+
+/* An Ethernet header, an IPv4 header and four octets of UDP ports */
+static const u8 udp_frame[] = {
+ 0x02, 0x00, 0x00, 0x00, 0x00, 0x02, /* destination address */
+ 0x02, 0x00, 0x00, 0x00, 0x00, 0x01, /* source address */
+ 0x08, 0x00,
+ 0x45, 0x00, 0x00, 0x20, 0xab, 0xcd, 0x00, 0x00,
+ 0x40, 0x11, 0x00, 0x00,
+ 192, 168, 1, 1,
+ 192, 168, 1, 2,
+ 0x13, 0x88, 0x27, 0x10,
+ 0xde, 0xad, 0xbe, 0xef,
+};
+
+/* The same datagram, as a fragment with a nonzero offset */
+static const u8 udp_fragment[] = {
+ 0x02, 0x00, 0x00, 0x00, 0x00, 0x02, /* destination address */
+ 0x02, 0x00, 0x00, 0x00, 0x00, 0x01, /* source address */
+ 0x08, 0x00,
+ 0x45, 0x00, 0x00, 0x20, 0xab, 0xcd, 0x00, 0x02,
+ 0x40, 0x11, 0x00, 0x00,
+ 192, 168, 1, 1,
+ 192, 168, 1, 2,
+ 0x13, 0x88, 0x27, 0x10,
+ 0xde, 0xad, 0xbe, 0xef,
+};
+
+static struct sk_buff *test_skb(struct kunit *test, const u8 *data,
+ size_t len)
+{
+ struct sk_buff *skb = kunit_zalloc_skb(test, 256, GFP_KERNEL);
+
+ KUNIT_ASSERT_NOT_NULL(test, skb);
+ skb_put_data(skb, data, len);
+
+ return skb;
+}
+
+#define TEST_SKB(test, buf) test_skb(test, buf, sizeof(buf))
+
+#define FLOW_F(name) BIT(CFG80211_FLOW_F_##name)
+
+#define QC(buf) ((const struct ieee80211_qos_char_elem *)(buf))
+#define QC_OK(buf) ieee80211_qos_char_size_ok(buf, sizeof(buf))
+
+/* A later fragment carries no ports */
+static void flow_parse_fragment(struct kunit *test)
+{
+ struct cfg80211_flow_info info;
+
+ KUNIT_ASSERT_TRUE(test,
+ cfg80211_flow_parse(TEST_SKB(test, udp_frame),
+ &info));
+ KUNIT_EXPECT_TRUE(test, info.present & FLOW_F(SRC_PORT));
+ KUNIT_EXPECT_TRUE(test, info.present & FLOW_F(DST_PORT));
+ KUNIT_EXPECT_EQ(test, be16_to_cpu(info.key.src_port), 5000);
+ KUNIT_EXPECT_EQ(test, be16_to_cpu(info.key.dst_port), 10000);
+ KUNIT_EXPECT_EQ(test, info.key.proto, IPPROTO_UDP);
+
+ KUNIT_ASSERT_TRUE(test,
+ cfg80211_flow_parse(TEST_SKB(test, udp_fragment),
+ &info));
+ KUNIT_EXPECT_FALSE(test, info.present & FLOW_F(SRC_PORT));
+ KUNIT_EXPECT_FALSE(test, info.present & FLOW_F(DST_PORT));
+ KUNIT_EXPECT_EQ(test, be16_to_cpu(info.key.src_port), 0);
+ KUNIT_EXPECT_EQ(test, be16_to_cpu(info.key.dst_port), 0);
+ KUNIT_EXPECT_TRUE(test, info.present & FLOW_F(IP_SRC));
+ KUNIT_EXPECT_TRUE(test, info.present & FLOW_F(IP_DST));
+}
+
+static struct cfg80211_scs_desc *
+test_scs_desc(struct kunit *test, u8 id, u8 up, const u8 *elems, size_t len)
+{
+ struct cfg80211_scs_desc *desc;
+ int n_tclas;
+
+ n_tclas = cfg80211_tclas_count(elems, len);
+ KUNIT_ASSERT_GE(test, n_tclas, 0);
+
+ desc = kunit_kzalloc(test, struct_size(desc, tclas, n_tclas),
+ GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, desc);
+
+ desc->id = id;
+ desc->up = up;
+ desc->n_tclas = n_tclas;
+ KUNIT_ASSERT_EQ(test, 0,
+ cfg80211_parse_tclas(elems, len, desc->tclas, n_tclas,
+ &desc->tclas_processing));
+
+ return desc;
+}
+
+/* When two descriptors match, the more granular one wins */
+static void scs_granularity(struct kunit *test)
+{
+ static const u8 two_params[] = {
+ WLAN_EID_TCLAS, 19,
+ 255,
+ 4, 0x06, /* type, mask: src and dst address */
+ 4, /* version */
+ 192, 168, 1, 1,
+ 192, 168, 1, 2,
+ 0, 0, 0, 0, /* ports, not selected */
+ 0, 0, 0, /* dscp, protocol, reserved */
+ };
+ static const u8 three_params[] = {
+ WLAN_EID_TCLAS, 19,
+ 255,
+ 4, 0x16, /* src, dst, dst port */
+ 4,
+ 192, 168, 1, 1,
+ 192, 168, 1, 2,
+ 0, 0, 0x27, 0x10,
+ 0, 0, 0,
+ };
+ static const u8 wrong_port[] = {
+ WLAN_EID_TCLAS, 19,
+ 255,
+ 4, 0x08, /* src port only */
+ 4,
+ 0, 0, 0, 0,
+ 0, 0, 0, 0,
+ 0x00, 0x35, 0, 0,
+ 0, 0, 0,
+ };
+ struct cfg80211_scs_desc *list[3];
+ struct cfg80211_scs_verdict verdict;
+ struct cfg80211_flow_info info;
+
+ list[0] = test_scs_desc(test, 1, 3, two_params, sizeof(two_params));
+ list[1] = test_scs_desc(test, 2, 6, three_params, sizeof(three_params));
+ list[2] = test_scs_desc(test, 3, 7, wrong_port, sizeof(wrong_port));
+
+ KUNIT_ASSERT_TRUE(test,
+ cfg80211_flow_parse(TEST_SKB(test, udp_frame),
+ &info));
+
+ /* The IP version is always matched, but does not count as a parameter */
+ KUNIT_EXPECT_EQ(test, list[0]->tclas[0].fields,
+ FLOW_F(IP_VERSION) | FLOW_F(IP_SRC) | FLOW_F(IP_DST));
+ KUNIT_EXPECT_EQ(test, list[1]->tclas[0].fields,
+ FLOW_F(IP_VERSION) | FLOW_F(IP_SRC) | FLOW_F(IP_DST) |
+ FLOW_F(DST_PORT));
+
+ cfg80211_scs_evaluate(list, 3, &info, &verdict);
+ KUNIT_EXPECT_TRUE(test, verdict.match);
+ KUNIT_EXPECT_EQ(test, verdict.scsid, 2);
+ KUNIT_EXPECT_EQ(test, verdict.up, 6);
+
+ swap(list[0], list[1]);
+ cfg80211_scs_evaluate(list, 3, &info, &verdict);
+ KUNIT_EXPECT_TRUE(test, verdict.match);
+ KUNIT_EXPECT_EQ(test, verdict.scsid, 2);
+
+ /* A frame without ports leaves only the address descriptor */
+ KUNIT_ASSERT_TRUE(test,
+ cfg80211_flow_parse(TEST_SKB(test, udp_fragment),
+ &info));
+ cfg80211_scs_evaluate(list, 3, &info, &verdict);
+ KUNIT_EXPECT_TRUE(test, verdict.match);
+ KUNIT_EXPECT_EQ(test, verdict.scsid, 1);
+ KUNIT_EXPECT_EQ(test, verdict.up, 3);
+}
+
+/* A key learned from an uplink frame must describe the downlink direction */
+static void flow_key_mirror(struct kunit *test)
+{
+ static const u8 mask_elem[] = {
+ WLAN_EID_EXTENSION, 19,
+ WLAN_EID_EXT_TCLAS_MASK,
+ 4, 0x1e,
+ 4,
+ 0, 0, 0, 0,
+ 0, 0, 0, 0,
+ 0, 0, 0, 0,
+ 0, 0, 0,
+ };
+ struct cfg80211_flow_key as_is, mirrored;
+ struct cfg80211_flow_info info;
+ u32 fields;
+
+ KUNIT_ASSERT_EQ(test, 0,
+ cfg80211_parse_tclas_mask(mask_elem, sizeof(mask_elem),
+ &fields));
+ KUNIT_EXPECT_EQ(test, fields,
+ FLOW_F(IP_VERSION) | FLOW_F(IP_SRC) | FLOW_F(IP_DST) |
+ FLOW_F(SRC_PORT) | FLOW_F(DST_PORT));
+
+ KUNIT_ASSERT_TRUE(test,
+ cfg80211_flow_parse(TEST_SKB(test, udp_frame),
+ &info));
+
+ KUNIT_ASSERT_TRUE(test, cfg80211_flow_key_build(&info, fields,
+ CFG80211_FLOW_AS_IS,
+ &as_is));
+ KUNIT_ASSERT_TRUE(test, cfg80211_flow_key_build(&info, fields,
+ CFG80211_FLOW_MIRRORED,
+ &mirrored));
+
+ KUNIT_EXPECT_MEMEQ(test, &as_is.src, &mirrored.dst, sizeof(as_is.src));
+ KUNIT_EXPECT_MEMEQ(test, &as_is.dst, &mirrored.src, sizeof(as_is.dst));
+ KUNIT_EXPECT_EQ(test, be16_to_cpu(as_is.src_port),
+ be16_to_cpu(mirrored.dst_port));
+ KUNIT_EXPECT_EQ(test, be16_to_cpu(as_is.dst_port),
+ be16_to_cpu(mirrored.src_port));
+
+ /* No unselected field may reach the key */
+ KUNIT_EXPECT_MEMEQ(test, as_is.sa, "\x00\x00\x00\x00\x00\x00",
+ ETH_ALEN);
+ KUNIT_EXPECT_EQ(test, be16_to_cpu(as_is.eth_type), 0);
+ KUNIT_EXPECT_EQ(test, as_is.proto, 0);
+ KUNIT_EXPECT_EQ(test, as_is.ip_version, 4);
+ KUNIT_EXPECT_EQ(test, mirrored.ip_version, 4);
+}
+
+/* The Ethernet addresses mirror as a pair */
+static void flow_key_mirror_eth(struct kunit *test)
+{
+ static const u8 mask_elem[] = {
+ WLAN_EID_EXTENSION, 17,
+ WLAN_EID_EXT_TCLAS_MASK,
+ 0, 0x01, /* classifier type 0, source address */
+ 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0,
+ 0, 0,
+ };
+ struct cfg80211_flow_key key;
+ struct cfg80211_flow_info info;
+ u32 fields;
+
+ KUNIT_ASSERT_EQ(test, 0,
+ cfg80211_parse_tclas_mask(mask_elem, sizeof(mask_elem),
+ &fields));
+ KUNIT_EXPECT_EQ(test, fields, FLOW_F(ETH_SA));
+
+ KUNIT_ASSERT_TRUE(test,
+ cfg80211_flow_parse(TEST_SKB(test, udp_frame),
+ &info));
+
+ KUNIT_ASSERT_TRUE(test, cfg80211_flow_key_build(&info, fields,
+ CFG80211_FLOW_MIRRORED,
+ &key));
+
+ KUNIT_EXPECT_MEMEQ(test, key.sa, test_da, ETH_ALEN);
+ KUNIT_EXPECT_MEMEQ(test, key.da, "\x00\x00\x00\x00\x00\x00", ETH_ALEN);
+}
+
+/* Each unsupported classifier is refused with -EOPNOTSUPP, not -EINVAL */
+static void tclas_refused(struct kunit *test)
+{
+ static const u8 mac_header[] = {
+ WLAN_EID_TCLAS, 5,
+ 255,
+ 6, 0x00, 0x00, 0x00, /* type 6, 3 octet mask */
+ };
+ static const u8 up_not_255[] = {
+ WLAN_EID_TCLAS, 17,
+ 0, /* a UP that takes part in the match */
+ 0, 0x01,
+ 0x02, 0x00, 0x00, 0x00, 0x00, 0x01,
+ 0x02, 0x00, 0x00, 0x00, 0x00, 0x02,
+ 0x08, 0x00,
+ };
+ static const u8 filter_in_mask[] = {
+ WLAN_EID_EXTENSION, 9,
+ WLAN_EID_EXT_TCLAS_MASK,
+ 3, 0,
+ 0, 0,
+ 0x00, 0x00,
+ 0xff, 0xff,
+ };
+ static const u8 legacy_ip[] = {
+ WLAN_EID_TCLAS, 19,
+ 255,
+ 1, 0x06,
+ 4,
+ 192, 168, 1, 1,
+ 192, 168, 1, 2,
+ 0, 0, 0, 0,
+ 0, 0, 0,
+ };
+ static const u8 legacy_vlan[] = {
+ WLAN_EID_TCLAS, 5,
+ 255,
+ 2, 0x01,
+ 0x00, 0x64,
+ };
+ enum cfg80211_tclas_processing processing;
+ struct cfg80211_tclas tclas;
+ u32 fields;
+
+ KUNIT_EXPECT_EQ(test, -EOPNOTSUPP,
+ cfg80211_parse_tclas(mac_header, sizeof(mac_header),
+ &tclas, 1, &processing));
+ KUNIT_EXPECT_EQ(test, -EOPNOTSUPP,
+ cfg80211_parse_tclas(up_not_255, sizeof(up_not_255),
+ &tclas, 1, &processing));
+ KUNIT_EXPECT_EQ(test, -EOPNOTSUPP,
+ cfg80211_parse_tclas(legacy_ip, sizeof(legacy_ip),
+ &tclas, 1, &processing));
+ KUNIT_EXPECT_EQ(test, -EOPNOTSUPP,
+ cfg80211_parse_tclas(legacy_vlan, sizeof(legacy_vlan),
+ &tclas, 1, &processing));
+ KUNIT_EXPECT_EQ(test, -EOPNOTSUPP,
+ cfg80211_parse_tclas_mask(filter_in_mask,
+ sizeof(filter_in_mask),
+ &fields));
+}
+
+/* The presence bitmap decides where every optional field starts */
+static void qos_characteristics(struct kunit *test)
+{
+ /* Downlink, TID 5, UP 5, and a Service Start Time with no LinkID */
+ static const u8 no_link_id[] = {
+ 0xff, 0x17, 0x71, /* element header, 113 */
+ 0x55, 0x05, 0x00, 0x00, /* control info, presence bit 1 */
+ 0xe8, 0x03, 0x00, 0x00, /* minimum service interval */
+ 0xd0, 0x07, 0x00, 0x00, /* maximum service interval */
+ 0x88, 0x13, 0x00, /* minimum data rate */
+ 0x20, 0x4e, 0x00, /* delay bound */
+ 0x78, 0x56, 0x34, 0x12, /* service start time */
+ };
+ static const u8 with_link_id[] = {
+ 0xff, 0x18, 0x71,
+ 0x55, 0x0d, 0x00, 0x00, /* presence bits 1 and 2 */
+ 0xe8, 0x03, 0x00, 0x00,
+ 0xd0, 0x07, 0x00, 0x00,
+ 0x88, 0x13, 0x00,
+ 0x20, 0x4e, 0x00,
+ 0x78, 0x56, 0x34, 0x12,
+ 0x03, /* service start time LinkID */
+ };
+ static const u8 orphan_link_id[] = {
+ 0xff, 0x14, 0x71,
+ 0x55, 0x09, 0x00, 0x00, /* presence bit 2 alone */
+ 0xe8, 0x03, 0x00, 0x00,
+ 0xd0, 0x07, 0x00, 0x00,
+ 0x88, 0x13, 0x00,
+ 0x20, 0x4e, 0x00,
+ 0x03,
+ };
+ static const u8 bad_ext_id[] = {
+ 0xff, 0x17, 0x70,
+ 0x55, 0x05, 0x00, 0x00,
+ 0xe8, 0x03, 0x00, 0x00,
+ 0xd0, 0x07, 0x00, 0x00,
+ 0x88, 0x13, 0x00,
+ 0x20, 0x4e, 0x00,
+ 0x78, 0x56, 0x34, 0x12,
+ };
+ static const u8 bad_length[] = {
+ 0xff, 0x18, 0x71,
+ 0x55, 0x05, 0x00, 0x00,
+ 0xe8, 0x03, 0x00, 0x00,
+ 0xd0, 0x07, 0x00, 0x00,
+ 0x88, 0x13, 0x00,
+ 0x20, 0x4e, 0x00,
+ 0x78, 0x56, 0x34, 0x12,
+ };
+ static const u8 short_elem[] = {
+ 0xff, 0x13, 0x71,
+ 0x55, 0x05, 0x00, 0x00,
+ 0xe8, 0x03, 0x00, 0x00,
+ 0xd0, 0x07, 0x00, 0x00,
+ 0x88, 0x13, 0x00,
+ 0x20, 0x4e, 0x00,
+ };
+
+ KUNIT_EXPECT_TRUE(test, QC_OK(no_link_id));
+ KUNIT_EXPECT_EQ(test, ieee80211_qos_char_direction(QC(no_link_id)),
+ IEEE80211_QOS_CHAR_DIR_DOWNLINK);
+ KUNIT_EXPECT_EQ(test, ieee80211_qos_char_presence(QC(no_link_id)),
+ IEEE80211_QOS_CHAR_PRES_SERVICE_START_TIME);
+
+ KUNIT_EXPECT_TRUE(test, QC_OK(with_link_id));
+ KUNIT_EXPECT_EQ(test, ieee80211_qos_char_presence(QC(with_link_id)),
+ IEEE80211_QOS_CHAR_PRES_SERVICE_START_TIME |
+ IEEE80211_QOS_CHAR_PRES_SERVICE_START_LINK_ID);
+
+ KUNIT_EXPECT_FALSE(test, QC_OK(orphan_link_id));
+ KUNIT_EXPECT_FALSE(test, QC_OK(short_elem));
+ KUNIT_EXPECT_FALSE(test, QC_OK(bad_ext_id));
+ KUNIT_EXPECT_FALSE(test, QC_OK(bad_length));
+}
+
+/* An uplink descriptor carries no classifier and no user priority */
+static void scs_traffic_description(struct kunit *test)
+{
+ /* Uplink, and a Service Start Time with no LinkID */
+ static const u8 uplink[] = {
+ 0xff, 0x17, 0x71,
+ 0x54, 0x05, 0x00, 0x00,
+ 0xe8, 0x03, 0x00, 0x00,
+ 0xd0, 0x07, 0x00, 0x00,
+ 0x88, 0x13, 0x00,
+ 0x20, 0x4e, 0x00,
+ 0x78, 0x56, 0x34, 0x12,
+ };
+ struct cfg80211_scs_desc desc = {
+ .req_type = NL80211_SCS_REQ_ADD,
+ .tclas_processing = CFG80211_TCLAS_PROCESSING_ABSENT,
+ .qos_char = QC(uplink),
+ .qos_char_len = sizeof(uplink),
+ .id = 1,
+ };
+
+ KUNIT_ASSERT_TRUE(test, QC_OK(uplink));
+ KUNIT_ASSERT_EQ(test, ieee80211_qos_char_direction(QC(uplink)),
+ IEEE80211_QOS_CHAR_DIR_UPLINK);
+
+ KUNIT_EXPECT_TRUE(test, cfg80211_scs_desc_valid(&desc));
+
+ /* Such a descriptor carries no TCLAS Processing element either */
+ desc.tclas_processing = CFG80211_TCLAS_PROCESSING_DEFAULT;
+ KUNIT_EXPECT_FALSE(test, cfg80211_scs_desc_valid(&desc));
+}
+
+/* A filter classifier matches on the payload octets of the named protocol */
+static void scs_filter(struct kunit *test)
+{
+ static const u8 udp_sport[] = {
+ WLAN_EID_TCLAS, 8,
+ 255,
+ 10, 0, 17, /* type, instance, protocol */
+ 0x13, 0x88, /* value: source port 5000 */
+ 0xff, 0xff, /* mask */
+ };
+ static const u8 wrong_sport[] = {
+ WLAN_EID_TCLAS, 8,
+ 255,
+ 10, 0, 17,
+ 0x00, 0x35,
+ 0xff, 0xff,
+ };
+ struct cfg80211_scs_desc *list[1];
+ struct cfg80211_scs_verdict verdict;
+ struct cfg80211_flow_info info;
+
+ KUNIT_ASSERT_TRUE(test,
+ cfg80211_flow_parse(TEST_SKB(test, udp_frame),
+ &info));
+
+ list[0] = test_scs_desc(test, 1, 5, udp_sport, sizeof(udp_sport));
+ KUNIT_EXPECT_EQ(test, list[0]->tclas[0].type, 10);
+ KUNIT_EXPECT_EQ(test, list[0]->tclas[0].filter.len, 2);
+
+ cfg80211_scs_evaluate(list, 1, &info, &verdict);
+ KUNIT_EXPECT_TRUE(test, verdict.match);
+ KUNIT_EXPECT_EQ(test, verdict.up, 5);
+
+ list[0] = test_scs_desc(test, 2, 5, wrong_sport, sizeof(wrong_sport));
+ cfg80211_scs_evaluate(list, 1, &info, &verdict);
+ KUNIT_EXPECT_FALSE(test, verdict.match);
+}
+
+static struct kunit_case scs_cases[] = {
+ KUNIT_CASE(flow_parse_fragment),
+ KUNIT_CASE(scs_granularity),
+ KUNIT_CASE(scs_filter),
+ KUNIT_CASE(flow_key_mirror),
+ KUNIT_CASE(flow_key_mirror_eth),
+ KUNIT_CASE(tclas_refused),
+ KUNIT_CASE(qos_characteristics),
+ KUNIT_CASE(scs_traffic_description),
+ {}
+};
+
+static struct kunit_suite scs = {
+ .name = "cfg80211-scs",
+ .test_cases = scs_cases,
+};
+
+kunit_test_suite(scs);
diff --git a/net/wireless/trace.h b/net/wireless/trace.h
index 8c2a91b85c396..81c0899cb212c 100644
--- a/net/wireless/trace.h
+++ b/net/wireless/trace.h
@@ -2636,6 +2636,55 @@ TRACE_EVENT(rdev_set_ap_chanwidth,
__entry->link_id)
);
+TRACE_EVENT(rdev_set_scs,
+ TP_PROTO(struct wiphy *wiphy, struct net_device *netdev,
+ const u8 *peer, u8 n_desc),
+ TP_ARGS(wiphy, netdev, peer, n_desc),
+ TP_STRUCT__entry(
+ WIPHY_ENTRY
+ NETDEV_ENTRY
+ MAC_ENTRY(peer)
+ __field(u8, n_desc)
+ ),
+ TP_fast_assign(
+ WIPHY_ASSIGN;
+ NETDEV_ASSIGN;
+ MAC_ASSIGN(peer, peer);
+ __entry->n_desc = n_desc;
+ ),
+ TP_printk(WIPHY_PR_FMT ", " NETDEV_PR_FMT ", %pM, %d descriptors",
+ WIPHY_PR_ARG, NETDEV_PR_ARG, __entry->peer, __entry->n_desc)
+);
+
+TRACE_EVENT(rdev_set_mscs,
+ TP_PROTO(struct wiphy *wiphy, struct net_device *netdev,
+ const u8 *peer, const struct cfg80211_mscs_desc *desc),
+ TP_ARGS(wiphy, netdev, peer, desc),
+ TP_STRUCT__entry(
+ WIPHY_ENTRY
+ NETDEV_ENTRY
+ MAC_ENTRY(peer)
+ __field(u32, fields)
+ __field(u32, stream_timeout)
+ __field(u8, up_bitmap)
+ __field(u8, up_limit)
+ ),
+ TP_fast_assign(
+ WIPHY_ASSIGN;
+ NETDEV_ASSIGN;
+ MAC_ASSIGN(peer, peer);
+ __entry->fields = desc ? desc->fields : 0;
+ __entry->stream_timeout = desc ? desc->stream_timeout : 0;
+ __entry->up_bitmap = desc ? desc->up_bitmap : 0;
+ __entry->up_limit = desc ? desc->up_limit : 0;
+ ),
+ TP_printk(WIPHY_PR_FMT ", " NETDEV_PR_FMT
+ ", %pM, fields %x, UP bitmap %x, UP limit %d, timeout %d",
+ WIPHY_PR_ARG, NETDEV_PR_ARG, __entry->peer, __entry->fields,
+ __entry->up_bitmap, __entry->up_limit,
+ __entry->stream_timeout)
+);
+
TRACE_EVENT(rdev_add_tx_ts,
TP_PROTO(struct wiphy *wiphy, struct net_device *netdev,
u8 tsid, const u8 *peer, u8 user_prio, u16 admitted_time),
|
| 31/3 | 2026/09/24 13:43 | llm | kmsan-evaluator |
1mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 59949 output: 23 thoughts: 24063 Thoughts: **KMSAN Fuzzing Evaluation for Stream Classification Service (SCS) Patch Series**
As an expert, I've meticulously reviewed this patch series introducing Stream Classification Service (SCS) and Mirrored Stream Classification Service (MSCS) support across various networking components. My goal is to determine if a dedicated KMSAN fuzzing session is warranted. I am focusing on the potential for uninitialized memory usage, information leaks, and other memory-related issues.
My analysis follows a strict criterion: I'm scrutinizing the code for instances where the patch introduces or modifies kernel structures sent to userspace (e.g., via `copy_to_user`, `put_user`, netlink attributes), uses potentially uninitialized variables in conditional logic, allocates and initializes complex data structures where uninitialized fields might be read later, or performs bounds checks/logic in a way that allows access to uninitialized buffer bytes.
I've carefully examined several key areas, starting with the netlink handlers in `nl80211`. I focused on the `nl80211_set_scs_reply` function. It transmits SCS descriptor information via netlink messages, and I traced its usage down to `nl80211_set_scs`. I observed a crucial pattern here: `kcalloc` allocates zeroed memory for the `desc` and `res` arrays. The `res[i].status` value is initially zero, and is later updated conditionally based on parsing and driver interaction. I carefully tracked the swaps and potential for uninitialized status codes to propagate, and concluded that those were safe, as the code correctly initializes and swaps arrays.
I've confirmed that the critical `struct cfg80211_flow_key` is also initialized using `memset`, preventing uninitialized memory reads or information leaks in the flow classification process. The structure's usage is carefully monitored in `cfg80211_flow_parse`, `cfg80211_flow_key_build`, `tclas_match`, `cfg80211_parse_frame_classifier`, and all relevant parts of the code. All code path are safe, as the structure is initialized to zero and the code also includes `static_assert` to ensure that this structure is correctly formed, avoiding padding issues.
I've thoroughly checked `ieee80211_flow_hkey` (used in hash tables), noting that the `cfg80211_flow_key` within the structure is always zeroed out before being used in hashing and comparison. I've confirmed that the rhashtable and debugfs use the correct lengths, preventing any out-of-bounds reads. I've also verified that the memory handling, particularly in functions like `kzalloc_obj` and the use of `kvzalloc`, ensures that the allocated memory is zero-initialized and any padding is also zeroed.
I also reviewed the `mt7996_mcu_set_scs` function, which constructs a command structure (`req`) to be sent to firmware. The structure is initialized using a designated initializer (`= { ... }`), ensuring that all fields are initialized to zero, preventing potential information leaks. The `ieee80211_qos_char_size_ok` function is properly checked in all code paths to confirm buffer bounds and avoid overflows or information leaks in QoS characteristic parsing.
My inspection also included netlink message creation and debugfs output, confirming that these functions use initialized data only. I investigated the code paths of `ieee80211_flow_classify`, `cfg80211_scs_evaluate`, and `cfg80211_flow_parse`, which all demonstrated zero initialization for structures. All code paths that allocate memory, or receive and transmit messages and data, have been validated and determined to be safe.
Finally, I’ve checked all memory access in `tclas_filter_offset`, `tclas_filter_match`, `cfg80211_flow_fields_mirror`, and `tclas_mask_fields`, and confirmed that all the variables are initialized correctly, the code correctly computes and access all indexes, avoiding any out-of-bounds reads or writes.
Based on my detailed review, the patch series appears to avoid the risks that would necessitate a dedicated KMSAN fuzzing session. The code employs robust initialization practices (e.g., `memset`, `kzalloc`, designated initializers) for all structures, ensuring data integrity and preventing the use of uninitialized memory.
|
| 32/3 | 2026/09/24 13:45 | tool | grepper |
0mArgs:{
"Expression": "nla_get_u8_default"
}Results: {
"Output": "drivers/net/gtp.c=1480=static int gtp_newlink(struct net_device *dev,\n--\ndrivers/net/gtp.c-1512-\ndrivers/net/gtp.c:1513:\tgtp-\u003erestart_count = nla_get_u8_default(data[IFLA_GTP_RESTART_COUNT],\ndrivers/net/gtp.c-1514-\t\t\t\t\t\t0);\n--\ndrivers/net/gtp.c=1839=static struct pdp_ctx *gtp_pdp_add(struct gtp_dev *gtp, struct sock *sk,\n--\ndrivers/net/gtp.c-1852-\ndrivers/net/gtp.c:1853:\tfamily = nla_get_u8_default(info-\u003eattrs[GTPA_FAMILY], AF_INET);\ndrivers/net/gtp.c-1854-\n--\ndrivers/net/gtp.c=2086=static struct pdp_ctx *gtp_find_pdp_by_link(struct net *net,\n--\ndrivers/net/gtp.c-2091-\ndrivers/net/gtp.c:2092:\tfamily = nla_get_u8_default(nla[GTPA_FAMILY], AF_INET);\ndrivers/net/gtp.c-2093-\n--\ndrivers/net/macsec.c=4345=static int macsec_validate_attr(struct nlattr *tb[], struct nlattr *data[],\n--\ndrivers/net/macsec.c-4368-\ndrivers/net/macsec.c:4369:\tes = nla_get_u8_default(data[IFLA_MACSEC_ES], false);\ndrivers/net/macsec.c:4370:\tsci = nla_get_u8_default(data[IFLA_MACSEC_INC_SCI], false);\ndrivers/net/macsec.c:4371:\tscb = nla_get_u8_default(data[IFLA_MACSEC_SCB], false);\ndrivers/net/macsec.c-4372-\n--\ninclude/net/netlink.h=1874=static inline u8 nla_get_u8(const struct nlattr *nla)\n--\ninclude/net/netlink.h-1879-/**\ninclude/net/netlink.h:1880: * nla_get_u8_default - return payload of u8 attribute or default\ninclude/net/netlink.h-1881- * @nla: u8 netlink attribute, may be %NULL\n--\ninclude/net/netlink.h-1885- */\ninclude/net/netlink.h:1886:static inline u8 nla_get_u8_default(const struct nlattr *nla, u8 defvalue)\ninclude/net/netlink.h-1887-{\n--\nnet/core/fib_rules.c=568=static int fib_nl2rule(struct net *net, struct nlmsghdr *nlh,\n--\nnet/core/fib_rules.c-607-\nnet/core/fib_rules.c:608:\tnlrule-\u003eproto = nla_get_u8_default(tb[FRA_PROTOCOL], RTPROT_UNSPEC);\nnet/core/fib_rules.c-609-\n--\nnet/devlink/dev.c=520=int devlink_nl_reload_doit(struct sk_buff *skb, struct genl_info *info)\n--\nnet/devlink/dev.c-534-\nnet/devlink/dev.c:535:\taction = nla_get_u8_default(info-\u003eattrs[DEVLINK_ATTR_RELOAD_ACTION],\nnet/devlink/dev.c-536-\t\t\t\t DEVLINK_RELOAD_ACTION_DRIVER_REINIT);\n--\nnet/hsr/hsr_netlink.c=32=static int hsr_newlink(struct net_device *dev,\n--\nnet/hsr/hsr_netlink.c-97-\nnet/hsr/hsr_netlink.c:98:\tmulticast_spec = nla_get_u8_default(data[IFLA_HSR_MULTICAST_SPEC], 0);\nnet/hsr/hsr_netlink.c-99-\n--\nnet/ieee802154/nl-mac.c=173=int ieee802154_associate_req(struct sk_buff *skb, struct genl_info *info)\n--\nnet/ieee802154/nl-mac.c-204-\nnet/ieee802154/nl-mac.c:205:\tpage = nla_get_u8_default(info-\u003eattrs[IEEE802154_ATTR_PAGE], 0);\nnet/ieee802154/nl-mac.c-206-\n--\nnet/ieee802154/nl-mac.c=292=int ieee802154_start_req(struct sk_buff *skb, struct genl_info *info)\n--\nnet/ieee802154/nl-mac.c-337-\nnet/ieee802154/nl-mac.c:338:\tpage = nla_get_u8_default(info-\u003eattrs[IEEE802154_ATTR_PAGE], 0);\nnet/ieee802154/nl-mac.c-339-\n--\nnet/ieee802154/nl-mac.c=361=int ieee802154_scan_req(struct sk_buff *skb, struct genl_info *info)\n--\nnet/ieee802154/nl-mac.c-384-\nnet/ieee802154/nl-mac.c:385:\tpage = nla_get_u8_default(info-\u003eattrs[IEEE802154_ATTR_PAGE], 0);\nnet/ieee802154/nl-mac.c-386-\n--\nnet/ieee802154/nl802154.c=1396=static int nl802154_trigger_scan(struct sk_buff *skb, struct genl_info *info)\n--\nnet/ieee802154/nl802154.c-1440-\t/* Use current page by default */\nnet/ieee802154/nl802154.c:1441:\trequest-\u003epage = nla_get_u8_default(info-\u003eattrs[NL802154_ATTR_PAGE],\nnet/ieee802154/nl802154.c-1442-\t\t\t\t\t wpan_phy-\u003ecurrent_page);\n--\nnet/ieee802154/nl802154.c-1450-\trequest-\u003eduration =\nnet/ieee802154/nl802154.c:1451:\t\tnla_get_u8_default(info-\u003eattrs[NL802154_ATTR_SCAN_DURATION],\nnet/ieee802154/nl802154.c-1452-\t\t\t\t IEEE802154_MAX_SCAN_DURATION);\n--\nnet/ieee802154/nl802154.c=1565=nl802154_send_beacons(struct sk_buff *skb, struct genl_info *info)\n--\nnet/ieee802154/nl802154.c-1596-\t/* Use maximum duration order by default */\nnet/ieee802154/nl802154.c:1597:\trequest-\u003einterval = nla_get_u8_default(info-\u003eattrs[NL802154_ATTR_BEACON_INTERVAL],\nnet/ieee802154/nl802154.c-1598-\t\t\t\t\t IEEE802154_MAX_SCAN_DURATION);\n--\nnet/ipv6/ila/ila_xlat.c=95=static int parse_nl_config(struct genl_info *info,\n--\nnet/ipv6/ila/ila_xlat.c-107-\nnet/ipv6/ila/ila_xlat.c:108:\txp-\u003eip.csum_mode = nla_get_u8_default(info-\u003eattrs[ILA_ATTR_CSUM_MODE],\nnet/ipv6/ila/ila_xlat.c-109-\t\t\t\t\t ILA_CSUM_NO_ACTION);\nnet/ipv6/ila/ila_xlat.c-110-\nnet/ipv6/ila/ila_xlat.c:111:\txp-\u003eip.ident_type = nla_get_u8_default(info-\u003eattrs[ILA_ATTR_IDENT_TYPE],\nnet/ipv6/ila/ila_xlat.c-112-\t\t\t\t\t ILA_ATYPE_USE_FORMAT);\n--\nnet/ipv6/ioam6_iptunnel.c=99=static int ioam6_build_state(struct net *net, struct nlattr *nla,\n--\nnet/ipv6/ioam6_iptunnel.c-137-\nnet/ipv6/ioam6_iptunnel.c:138:\tmode = nla_get_u8_default(tb[IOAM6_IPTUNNEL_MODE],\nnet/ipv6/ioam6_iptunnel.c-139-\t\t\t\t IOAM6_IPTUNNEL_MODE_INLINE);\n--\nnet/netfilter/nft_tunnel.c=419=static int nft_tunnel_obj_init(const struct nft_ctx *ctx,\n--\nnet/netfilter/nft_tunnel.c-473-\t\tinfo.key.tos = nla_get_u8(tb[NFTA_TUNNEL_KEY_TOS]);\nnet/netfilter/nft_tunnel.c:474:\tinfo.key.ttl = nla_get_u8_default(tb[NFTA_TUNNEL_KEY_TTL], U8_MAX);\nnet/netfilter/nft_tunnel.c-475-\n--\nnet/sched/act_ct.c=1259=static int tcf_ct_fill_params(struct net *net,\n--\nnet/sched/act_ct.c-1345-\t\t}\nnet/sched/act_ct.c:1346:\t\tfamily = nla_get_u8_default(tb[TCA_CT_HELPER_FAMILY], AF_INET);\nnet/sched/act_ct.c:1347:\t\tproto = nla_get_u8_default(tb[TCA_CT_HELPER_PROTO],\nnet/sched/act_ct.c-1348-\t\t\t\t\t IPPROTO_TCP);\n--\nnet/sched/act_mpls.c=158=static int tcf_mpls_init(struct net *net, struct nlattr *nla,\n--\nnet/sched/act_mpls.c-290-\t\t\t\t\t ACT_MPLS_LABEL_NOT_SET);\nnet/sched/act_mpls.c:291:\tp-\u003etcfm_tc = nla_get_u8_default(tb[TCA_MPLS_TC], ACT_MPLS_TC_NOT_SET);\nnet/sched/act_mpls.c:292:\tp-\u003etcfm_ttl = nla_get_u8_default(tb[TCA_MPLS_TTL], mpls_ttl);\nnet/sched/act_mpls.c:293:\tp-\u003etcfm_bos = nla_get_u8_default(tb[TCA_MPLS_BOS],\nnet/sched/act_mpls.c-294-\t\t\t\t\t ACT_MPLS_BOS_NOT_SET);\n--\nnet/wireless/nl80211.c=1607=static unsigned int nl80211_link_id(struct nlattr **attrs)\n--\nnet/wireless/nl80211.c-1610-\nnet/wireless/nl80211.c:1611:\treturn nla_get_u8_default(linkid, 0);\nnet/wireless/nl80211.c-1612-}\n--\nnet/wireless/nl80211.c=12997=static int nl80211_crypto_settings(struct cfg80211_registered_device *rdev,\n--\nnet/wireless/nl80211.c-13107-\tsettings-\u003esae_pwe =\nnet/wireless/nl80211.c:13108:\t\tnla_get_u8_default(info-\u003eattrs[NL80211_ATTR_SAE_PWE],\nnet/wireless/nl80211.c-13109-\t\t\t\t NL80211_SAE_PWE_UNSPECIFIED);\n--\nnet/wireless/nl80211.c=17573=static int nl80211_nan_set_peer_sched(struct sk_buff *skb,\n--\nnet/wireless/nl80211.c-17654-\tsched.peer_addr = nla_data(info-\u003eattrs[NL80211_ATTR_MAC]);\nnet/wireless/nl80211.c:17655:\tsched.seq_id = nla_get_u8_default(info-\u003eattrs[NL80211_ATTR_NAN_SEQ_ID], 0);\nnet/wireless/nl80211.c-17656-\tsched.committed_dw = nla_get_u16(info-\u003eattrs[NL80211_ATTR_NAN_COMMITTED_DW]);\n--\nnet/wireless/nl80211.c=18371=nl80211_parse_scs_desc(struct nlattr *attr, struct cfg80211_scs_result *res,\n--\nnet/wireless/nl80211.c-18431-\tdesc-\u003ereq_type = nla_get_u8(tb[NL80211_SCS_DESC_ATTR_REQ_TYPE]);\nnet/wireless/nl80211.c:18432:\tdesc-\u003eup = nla_get_u8_default(tb[NL80211_SCS_DESC_ATTR_UP], 0);\nnet/wireless/nl80211.c-18433-\tdesc-\u003en_tclas = n_tclas;\n--\nnet/wireless/nl80211.c=18620=static int nl80211_set_mscs(struct sk_buff *skb, struct genl_info *info)\n--\nnet/wireless/nl80211.c-18669-\tdesc.up_bitmap =\nnet/wireless/nl80211.c:18670:\t\tnla_get_u8_default(tb[NL80211_MSCS_DESC_ATTR_UP_BITMAP], 0);\nnet/wireless/nl80211.c:18671:\tdesc.up_limit = nla_get_u8_default(tb[NL80211_MSCS_DESC_ATTR_UP_LIMIT],\nnet/wireless/nl80211.c-18672-\t\t\t\t\t 7);\n--\nnet/xfrm/xfrm_user.c=317=static int verify_newsa_info(struct xfrm_usersa_info *p,\n--\nnet/xfrm/xfrm_user.c-321-\tint err;\nnet/xfrm/xfrm_user.c:322:\tu8 sa_dir = nla_get_u8_default(attrs[XFRMA_SA_DIR], 0);\nnet/xfrm/xfrm_user.c-323-\tu16 family = p-\u003esel.family;\n"
} |
| 33/3 | 2026/09/24 13:45 | llm | kmsan-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 89093 output: 24 thoughts: 42 Thoughts: Okay, here's my take on those thoughts, framed as an expert's self-reflection: **Inspecting Attribute Retrieval in Netlink with Defaults** Alright, so `nla_get_u8_default` is designed to be a safe way to retrieve an unsigned 8-bit integer attribute from a Netlink message, and crucially, to provide a default value if the attribute isn't present, or if it's NULL. That's good, that's what we want – prevents a potential null pointer dereference, which is always something to be avoided. A well-designed function will always handle NULL gracefully. Now, I should make sure that the `nla_get_u32_default` function is written in a safe manner as well. It follows the same pattern, but I need to make sure the same protection is in place. |
| 34/3 | 2026/09/24 13:45 | tool | grepper |
0mArgs:{
"Expression": "nla_get_u32_default"
}Results: {
"Output": "drivers/net/vxlan/vxlan_core.c=1152=static int vxlan_fdb_parse(struct nlattr *tb[], struct vxlan_dev *vxlan,\n--\ndrivers/net/vxlan/vxlan_core.c-1232-\ndrivers/net/vxlan/vxlan_core.c:1233:\t*nhid = nla_get_u32_default(tb[NDA_NH_ID], 0);\ndrivers/net/vxlan/vxlan_core.c-1234-\n--\ninclude/net/netlink.h=1732=static inline u32 nla_get_u32(const struct nlattr *nla)\n--\ninclude/net/netlink.h-1737-/**\ninclude/net/netlink.h:1738: * nla_get_u32_default - return payload of u32 attribute or default\ninclude/net/netlink.h-1739- * @nla: u32 netlink attribute, may be %NULL\n--\ninclude/net/netlink.h-1743- */\ninclude/net/netlink.h:1744:static inline u32 nla_get_u32_default(const struct nlattr *nla, u32 defvalue)\ninclude/net/netlink.h-1745-{\n--\nnet/ethtool/rss.c=582=ethnl_rss_set_validate(struct ethnl_req_info *req_info, struct genl_info *info)\n--\nnet/ethtool/rss.c-598-\nnet/ethtool/rss.c:599:\tinput_xfrm = nla_get_u32_default(tb[ETHTOOL_A_RSS_INPUT_XFRM], 0);\nnet/ethtool/rss.c-600-\tif (input_xfrm \u0026 ~ops-\u003esupported_input_xfrm)\n--\nnet/ethtool/rss.c=946=ethnl_rss_create_validate(struct net_device *dev, struct genl_info *info)\n--\nnet/ethtool/rss.c-955-\nnet/ethtool/rss.c:956:\trss_context = nla_get_u32_default(tb[ETHTOOL_A_RSS_CONTEXT], 0);\nnet/ethtool/rss.c-957-\tif (ops-\u003erxfh_max_num_contexts \u0026\u0026\n--\nnet/ethtool/rss.c-968-\nnet/ethtool/rss.c:969:\tinput_xfrm = nla_get_u32_default(tb[ETHTOOL_A_RSS_INPUT_XFRM], 0);\nnet/ethtool/rss.c-970-\tif (input_xfrm \u0026 ~ops-\u003esupported_input_xfrm)\n--\nnet/ethtool/rss.c=1002=int ethnl_rss_create_doit(struct sk_buff *skb, struct genl_info *info)\n--\nnet/ethtool/rss.c-1030-\nnet/ethtool/rss.c:1031:\treq.rss_context = nla_get_u32_default(tb[ETHTOOL_A_RSS_CONTEXT], 0);\nnet/ethtool/rss.c-1032-\n--\nnet/ieee802154/nl802154.c=1396=static int nl802154_trigger_scan(struct sk_buff *skb, struct genl_info *info)\n--\nnet/ieee802154/nl802154.c-1445-\trequest-\u003echannels =\nnet/ieee802154/nl802154.c:1446:\t\tnla_get_u32_default(info-\u003eattrs[NL802154_ATTR_SCAN_CHANNELS],\nnet/ieee802154/nl802154.c-1447-\t\t\t\t wpan_phy-\u003esupported.channels[request-\u003epage]);\n--\nnet/ipv4/devinet.c=884=static struct in_ifaddr *inet_rtm_to_ifa(struct net *net, struct nlmsghdr *nlh,\n--\nnet/ipv4/devinet.c-921-\tifa-\u003eifa_mask = inet_make_mask(ifm-\u003eifa_prefixlen);\nnet/ipv4/devinet.c:922:\tifa-\u003eifa_flags = nla_get_u32_default(tb[IFA_FLAGS], ifm-\u003eifa_flags);\nnet/ipv4/devinet.c-923-\tifa-\u003eifa_scope = ifm-\u003eifa_scope;\n--\nnet/ipv4/ipmr.c=2732=static int ipmr_rtm_getroute(struct sk_buff *in_skb, struct nlmsghdr *nlh,\n--\nnet/ipv4/ipmr.c-2749-\tgrp = nla_get_in_addr_default(tb[RTA_DST], 0);\nnet/ipv4/ipmr.c:2750:\ttableid = nla_get_u32_default(tb[RTA_TABLE], 0);\nnet/ipv4/ipmr.c-2751-\n--\nnet/ipv4/nexthop.c=3360=static int nh_valid_get_del_req(const struct nlmsghdr *nlh,\n--\nnet/ipv4/nexthop.c-3382-\tif (op_flags)\nnet/ipv4/nexthop.c:3383:\t\t*op_flags = nla_get_u32_default(tb[NHA_OP_FLAGS], 0);\nnet/ipv4/nexthop.c-3384-\n--\nnet/ipv4/nexthop.c=3555=static int nh_valid_dump_req(const struct nlmsghdr *nlh,\n--\nnet/ipv4/nexthop.c-3567-\nnet/ipv4/nexthop.c:3568:\tfilter-\u003eop_flags = nla_get_u32_default(tb[NHA_OP_FLAGS], 0);\nnet/ipv4/nexthop.c-3569-\n--\nnet/ipv4/route.c=3319=static int inet_rtm_getroute(struct sk_buff *in_skb, struct nlmsghdr *nlh,\n--\nnet/ipv4/route.c-3346-\tdst = nla_get_in_addr_default(tb[RTA_DST], 0);\nnet/ipv4/route.c:3347:\tiif = nla_get_u32_default(tb[RTA_IIF], 0);\nnet/ipv4/route.c:3348:\tmark = nla_get_u32_default(tb[RTA_MARK], 0);\nnet/ipv4/route.c-3349-\tdscp = inet_dsfield_to_dscp(rtm-\u003ertm_tos);\n--\nnet/ipv4/route.c-3374-\tfl4.flowi4_dscp = dscp;\nnet/ipv4/route.c:3375:\tfl4.flowi4_oif = nla_get_u32_default(tb[RTA_OIF], 0);\nnet/ipv4/route.c-3376-\tfl4.flowi4_mark = mark;\n--\nnet/ipv6/addrconf.c=4815=inet6_rtm_deladdr(struct sk_buff *skb, struct nlmsghdr *nlh,\n--\nnet/ipv6/addrconf.c-4834-\nnet/ipv6/addrconf.c:4835:\tifa_flags = nla_get_u32_default(tb[IFA_FLAGS], ifm-\u003eifa_flags);\nnet/ipv6/addrconf.c-4836-\n--\nnet/ipv6/addrconf.c=4990=inet6_rtm_newaddr(struct sk_buff *skb, struct nlmsghdr *nlh,\n--\nnet/ipv6/addrconf.c-5025-\nnet/ipv6/addrconf.c:5026:\tcfg.ifa_flags = nla_get_u32_default(tb[IFA_FLAGS], ifm-\u003eifa_flags);\nnet/ipv6/addrconf.c-5027-\n--\nnet/ipv6/ioam6.c=107=static int ioam6_genl_addns(struct sk_buff *skb, struct genl_info *info)\n--\nnet/ipv6/ioam6.c-137-\nnet/ipv6/ioam6.c:138:\tdata32 = nla_get_u32_default(info-\u003eattrs[IOAM6_ATTR_NS_DATA],\nnet/ipv6/ioam6.c-139-\t\t\t\t IOAM6_U32_UNAVAILABLE);\n--\nnet/ipv6/ip6mr.c=2734=static int ip6mr_rtm_getroute(struct sk_buff *in_skb, struct nlmsghdr *nlh,\n--\nnet/ipv6/ip6mr.c-2757-\t\tgrp = nla_get_in6_addr(tb[RTA_DST]);\nnet/ipv6/ip6mr.c:2758:\ttableid = nla_get_u32_default(tb[RTA_TABLE], 0);\nnet/ipv6/ip6mr.c-2759-\n--\nnet/openvswitch/datapath.c=614=static int ovs_packet_cmd_execute(struct sk_buff *skb, struct genl_info *info)\n--\nnet/openvswitch/datapath.c-661-\tOVS_CB(packet)-\u003eupcall_pid =\nnet/openvswitch/datapath.c:662:\t\tnla_get_u32_default(a[OVS_PACKET_ATTR_UPCALL_PID], 0);\nnet/openvswitch/datapath.c-663-\n--\nnet/openvswitch/datapath.c=2334=static int ovs_vport_cmd_new(struct sk_buff *skb, struct genl_info *info)\n--\nnet/openvswitch/datapath.c-2354-\nnet/openvswitch/datapath.c:2355:\tport_no = nla_get_u32_default(a[OVS_VPORT_ATTR_PORT_NO], 0);\nnet/openvswitch/datapath.c-2356-\tif (port_no \u003e= DP_MAX_PORTS)\n--\nnet/openvswitch/flow_netlink.c=1902=u32 ovs_nla_get_ufid_flags(const struct nlattr *attr)\nnet/openvswitch/flow_netlink.c-1903-{\nnet/openvswitch/flow_netlink.c:1904:\treturn nla_get_u32_default(attr, 0);\nnet/openvswitch/flow_netlink.c-1905-}\n--\nnet/sched/act_ctinfo.c=155=static int tcf_ctinfo_init(struct net *net, struct nlattr *nla,\n--\nnet/sched/act_ctinfo.c-199-\t\tdscpstatemask =\nnet/sched/act_ctinfo.c:200:\t\t\tnla_get_u32_default(tb[TCA_CTINFO_PARMS_DSCP_STATEMASK],\nnet/sched/act_ctinfo.c-201-\t\t\t\t\t 0);\n--\nnet/sched/act_mpls.c=158=static int tcf_mpls_init(struct net *net, struct nlattr *nla,\n--\nnet/sched/act_mpls.c-288-\tp-\u003etcfm_action = parm-\u003em_action;\nnet/sched/act_mpls.c:289:\tp-\u003etcfm_label = nla_get_u32_default(tb[TCA_MPLS_LABEL],\nnet/sched/act_mpls.c-290-\t\t\t\t\t ACT_MPLS_LABEL_NOT_SET);\n--\nnet/sched/cls_api.c=2257=static int tc_new_tfilter(struct sk_buff *skb, struct nlmsghdr *n,\n--\nnet/sched/cls_api.c-2348-\nnet/sched/cls_api.c:2349:\tchain_index = nla_get_u32_default(tca[TCA_CHAIN], 0);\nnet/sched/cls_api.c-2350-\tif (chain_index \u003e TC_ACT_EXT_VAL_MASK) {\n--\nnet/sched/cls_api.c=2495=static int tc_del_tfilter(struct sk_buff *skb, struct nlmsghdr *n,\n--\nnet/sched/cls_api.c-2563-\nnet/sched/cls_api.c:2564:\tchain_index = nla_get_u32_default(tca[TCA_CHAIN], 0);\nnet/sched/cls_api.c-2565-\tif (chain_index \u003e TC_ACT_EXT_VAL_MASK) {\n--\nnet/sched/cls_api.c=2654=static int tc_get_tfilter(struct sk_buff *skb, struct nlmsghdr *n,\n--\nnet/sched/cls_api.c-2721-\nnet/sched/cls_api.c:2722:\tchain_index = nla_get_u32_default(tca[TCA_CHAIN], 0);\nnet/sched/cls_api.c-2723-\tif (chain_index \u003e TC_ACT_EXT_VAL_MASK) {\n--\nnet/sched/cls_api.c=3136=static int tc_ctl_chain(struct sk_buff *skb, struct nlmsghdr *n,\n--\nnet/sched/cls_api.c-3165-\nnet/sched/cls_api.c:3166:\tchain_index = nla_get_u32_default(tca[TCA_CHAIN], 0);\nnet/sched/cls_api.c-3167-\tif (chain_index \u003e TC_ACT_EXT_VAL_MASK) {\n--\nnet/sched/sch_choke.c=339=static int choke_change(struct Qdisc *sch, struct nlattr *opt,\n--\nnet/sched/sch_choke.c-362-\nnet/sched/sch_choke.c:363:\tmax_P = nla_get_u32_default(tb[TCA_CHOKE_MAX_P], 0);\nnet/sched/sch_choke.c-364-\n--\nnet/sched/sch_gred.c=642=static int gred_change(struct Qdisc *sch, struct nlattr *opt,\n--\nnet/sched/sch_gred.c-670-\nnet/sched/sch_gred.c:671:\tmax_P = nla_get_u32_default(tb[TCA_GRED_MAX_P], 0);\nnet/sched/sch_gred.c-672-\n--\nnet/sched/sch_qfq.c=406=static int qfq_change_class(struct Qdisc *sch, u32 classid, u32 parentid,\n--\nnet/sched/sch_qfq.c-428-\nnet/sched/sch_qfq.c:429:\tweight = nla_get_u32_default(tb[TCA_QFQ_WEIGHT], 1);\nnet/sched/sch_qfq.c-430-\n--\nnet/sched/sch_red.c=243=static int __red_change(struct Qdisc *sch, struct nlattr **tb,\n--\nnet/sched/sch_red.c-259-\nnet/sched/sch_red.c:260:\tmax_P = nla_get_u32_default(tb[TCA_RED_MAX_P], 0);\nnet/sched/sch_red.c-261-\n--\nnet/sched/sch_taprio.c=1822=static int taprio_change(struct Qdisc *sch, struct nlattr *opt,\n--\nnet/sched/sch_taprio.c-1849-\t */\nnet/sched/sch_taprio.c:1850:\ttaprio_flags = nla_get_u32_default(tb[TCA_TAPRIO_ATTR_FLAGS], 0);\nnet/sched/sch_taprio.c-1851-\n--\nnet/wireless/nl80211.c=3957=static int _nl80211_parse_chandef(struct cfg80211_registered_device *rdev,\n--\nnet/wireless/nl80211.c-4036-\t\t\t\tnla_get_u32(attrs[NL80211_ATTR_CENTER_FREQ1]);\nnet/wireless/nl80211.c:4037:\t\t\tchandef-\u003efreq1_offset = nla_get_u32_default(\nnet/wireless/nl80211.c-4038-\t\t\t\tattrs[NL80211_ATTR_CENTER_FREQ1_OFFSET], 0);\n--\nnet/wireless/nl80211.c-4093-\t\tchandef-\u003enpca_punctured =\nnet/wireless/nl80211.c:4094:\t\t\tnla_get_u32_default(attrs[NL80211_ATTR_NPCA_PUNCT_BITMAP],\nnet/wireless/nl80211.c-4095-\t\t\t\t\t chandef-\u003epunctured);\n--\nnet/wireless/nl80211.c=10231=static int nl80211_req_set_reg(struct sk_buff *skb, struct genl_info *info)\n--\nnet/wireless/nl80211.c-10247-\tuser_reg_hint_type =\nnet/wireless/nl80211.c:10248:\t\tnla_get_u32_default(info-\u003eattrs[NL80211_ATTR_USER_REG_HINT_TYPE],\nnet/wireless/nl80211.c-10249-\t\t\t\t NL80211_USER_REG_HINT_USER);\n--\nnet/wireless/nl80211.c=15877=static int nl80211_set_wowlan(struct sk_buff *skb, struct genl_info *info)\n--\nnet/wireless/nl80211.c-16000-\t\t\tpkt_offset =\nnet/wireless/nl80211.c:16001:\t\t\t\tnla_get_u32_default(pat_tb[NL80211_PKTPAT_OFFSET],\nnet/wireless/nl80211.c-16002-\t\t\t\t\t\t 0);\n--\nnet/wireless/nl80211.c=16182=static int nl80211_parse_coalesce_rule(struct cfg80211_registered_device *rdev,\n--\nnet/wireless/nl80211.c-16244-\nnet/wireless/nl80211.c:16245:\t\tpkt_offset = nla_get_u32_default(pat_tb[NL80211_PKTPAT_OFFSET],\nnet/wireless/nl80211.c-16246-\t\t\t\t\t\t 0);\n--\nnet/wireless/nl80211.c=18620=static int nl80211_set_mscs(struct sk_buff *skb, struct genl_info *info)\n--\nnet/wireless/nl80211.c-18673-\tdesc.stream_timeout =\nnet/wireless/nl80211.c:18674:\t\tnla_get_u32_default(tb[NL80211_MSCS_DESC_ATTR_STREAM_TIMEOUT],\nnet/wireless/nl80211.c-18675-\t\t\t\t 0);\n--\nnet/xfrm/xfrm_user.c=896=static void xfrm_smark_init(struct nlattr **attrs, struct xfrm_mark *m)\n--\nnet/xfrm/xfrm_user.c-899-\t\tm-\u003ev = nla_get_u32(attrs[XFRMA_SET_MARK]);\nnet/xfrm/xfrm_user.c:900:\t\tm-\u003em = nla_get_u32_default(attrs[XFRMA_SET_MARK_MASK],\nnet/xfrm/xfrm_user.c-901-\t\t\t\t\t 0xffffffff);\n--\nnet/xfrm/xfrm_user.c=3335=static int xfrm_do_migrate_state(struct sk_buff *skb, struct nlmsghdr *nlh,\n--\nnet/xfrm/xfrm_user.c-3415-\nnet/xfrm/xfrm_user.c:3416:\tif (nla_get_u32_default(attrs[XFRMA_NAT_KEEPALIVE_INTERVAL], 0) \u0026\u0026 !m.encap) {\nnet/xfrm/xfrm_user.c-3417-\t\tNL_SET_ERR_MSG(extack,\n--\nnet/xfrm/xfrm_user.c-3441-\nnet/xfrm/xfrm_user.c:3442:\tm.mapping_maxage = nla_get_u32_default(attrs[XFRMA_MTIMER_THRESH],\nnet/xfrm/xfrm_user.c-3443-\t\t\t\t\t x-\u003emapping_maxage);\nnet/xfrm/xfrm_user.c:3444:\tm.nat_keepalive_interval = nla_get_u32_default(attrs[XFRMA_NAT_KEEPALIVE_INTERVAL],\nnet/xfrm/xfrm_user.c-3445-\t\t\t\t\t\t x-\u003enat_keepalive_interval);\n"
} |
| 35/3 | 2026/09/24 13:45 | llm | kmsan-evaluator |
0mModel:gemini-3.8-flash Args: null Results: null Tokens: input: 95041 output: 559 thoughts: 1546 Thoughts: **Memory Management & KMSAN Analysis - This Patch**
Okay, let's break this down systematically. I need to be *absolutely* sure about the memory safety implications of these changes, particularly in relation to uninitialized memory. My initial instinct is that KMSAN isn't needed here, but I need to justify that conclusion rigorously.
First, I've gone through the code meticulously, focusing on memory allocation and initialization. I see that every single heap object ( `cfg80211_scs_desc`, `cfg80211_scs_result`, `ieee80211_scs_sta`, `ieee80211_mscs_sta`, `ieee80211_flow_entry` ) is allocated with either `kzalloc`, `kvzalloc`, `kcalloc`, or `kzalloc_obj`. That's critical because `kzalloc` guarantees zero-initialization. Furthermore, all stack-allocated structures like `cfg80211_flow_info`, `cfg80211_flow_key`, and `cfg80211_scs_verdict`, as well as those in the firmware command (`mt7996_mcu_set_scs`) are either zeroed via `= {}` or explicitly with `memset(..., 0, ...)`. That's the first major check, and it looks solid.
Next, I examined how data is communicated to userspace through netlink. The analysis shows the patch writes discrete scalar values such as `desc[i]->id` (u8) and `res[i].status` (u16) as netlink attributes. I don't see any cases where raw structs or potentially uninitialized buffers, including padding, are being copied directly to user space. That's a key point. This eliminates the risk of unintentional information leaks.
I also looked for any instances of uninitialized variable reads. The analysis indicates potential, but subsequent use guarantees proper initialization.
Finally, I reviewed the KMSAN criteria again. The patch doesn't introduce any kernel structures that are sent to user space with potential uninitialized fields or padding, it avoids any conditional logic relying on potentially uninitialized variables or fields, and it completely avoids introducing allocation or initialization of complex data structures that might lead to later uninitialized field reads in reachable code paths, and there are no instances of bounds checks or length that could allow access to uninitialized bytes in existing buffers. Any potential bugs this patch might introduce are out-of-bounds reads/writes, buffer bounds errors, reference count / locking errors, or UAFs. Those are caught by KASAN and LOCKDEP.
So, in summary: Every relevant allocation path is initialized and zeroed before use, there is no direct copying of structures with potential uninitialized padding to userspace and all stack variables are explicitly zeroed. The patch *specifically* avoids the KMSAN-triggering scenarios. Therefore, I'm confident that `NeedsKMSAN` should be false.
```tool_code
set-results: { "NeedsKMSAN": false, "KMSANReasoning": "The patch meticulously ensures that all heap and stack allocated structures are either zero-initialized via kzalloc, kcalloc, kvzalloc, kzalloc_obj, or initialized with = {}. Furthermore there is no direct copying of kernel structures with potential uninitialized padding to userspace, with scalar values only being copied using netlink and other attributes. All stack variables are also explicitly zeroed or initialized. There are no instances of bounds checks or length that could allow access to uninitialized bytes in existing buffers, and any potential memory safety bugs introduced here would be out-of-bounds reads/writes, buffer bounds errors, reference count / locking errors, or UAFs (which are covered by KASAN and LOCKDEP), not uninitialized memory bugs. Therefore, KMSAN is not required." }
```
|
| 36/3 | 2026/09/24 13:45 | tool | set-results |
0mArgs:{
"KMSANReasoning": "The patch series implements Stream Classification Service (SCS) and Mirrored SCS (MSCS) support across cfg80211, mac80211, and the mt7996 / mac80211_hwsim drivers.\n\nKMSAN vs KASAN Applicability Analysis:\n1. User-space copies / Info-leaks: Responses to netlink commands (NL80211_CMD_SET_SCS and NL80211_CMD_SET_MSCS) send discrete scalar values using nla_put_u8 and nla_put_u16 rather than writing entire structures or aggregates containing padding bytes. There are no kernel-to-user info-leak risks.\n2. Structure allocations and initializations: All newly introduced heap allocations (such as cfg80211_scs_desc, ieee80211_scs_sta, ieee80211_mscs_sta, and ieee80211_flow_entry) are allocated with kzalloc, kvzalloc, kcalloc, or kzalloc_obj. Stack structures (e.g., cfg80211_flow_info, cfg80211_flow_key, cfg80211_tclas, cfg80211_mscs_desc) are consistently zeroed either at declaration via '= {}' or through explicit memset() calls before parsing and manipulation.\n3. Flow key comparison and hashing: Structures compared with memcmp or hashed via rhashtable (cfg80211_flow_key and ieee80211_flow_hkey) have their layouts verified with static_assert against padding, and rhashtable key lengths strictly bound the lookup key length to offsetofend(..., key).\n4. Firmware communication: In mt7996_mcu_set_scs, the firmware request struct is initialized with designated initializers '= { ... }', ensuring all padding and reserved fields are zero-filled.\n5. Bug profile: The primary risk surface of the added element parsers, frame classifiers, and driver callbacks involves out-of-bounds array reads, parser boundary validation, concurrency/locking, or object lifetime management. These failure modes are comprehensively covered by KASAN, UBSAN, and LOCKDEP.\n\nBecause no uninitialized memory usage or info-leak vectors are introduced, a dedicated KMSAN fuzzing session is not justified.",
"NeedsKMSAN": false
}Results: {
"KMSANReasoning": "The patch series implements Stream Classification Service (SCS) and Mirrored SCS (MSCS) support across cfg80211, mac80211, and the mt7996 / mac80211_hwsim drivers.\n\nKMSAN vs KASAN Applicability Analysis:\n1. User-space copies / Info-leaks: Responses to netlink commands (NL80211_CMD_SET_SCS and NL80211_CMD_SET_MSCS) send discrete scalar values using nla_put_u8 and nla_put_u16 rather than writing entire structures or aggregates containing padding bytes. There are no kernel-to-user info-leak risks.\n2. Structure allocations and initializations: All newly introduced heap allocations (such as cfg80211_scs_desc, ieee80211_scs_sta, ieee80211_mscs_sta, and ieee80211_flow_entry) are allocated with kzalloc, kvzalloc, kcalloc, or kzalloc_obj. Stack structures (e.g., cfg80211_flow_info, cfg80211_flow_key, cfg80211_tclas, cfg80211_mscs_desc) are consistently zeroed either at declaration via '= {}' or through explicit memset() calls before parsing and manipulation.\n3. Flow key comparison and hashing: Structures compared with memcmp or hashed via rhashtable (cfg80211_flow_key and ieee80211_flow_hkey) have their layouts verified with static_assert against padding, and rhashtable key lengths strictly bound the lookup key length to offsetofend(..., key).\n4. Firmware communication: In mt7996_mcu_set_scs, the firmware request struct is initialized with designated initializers '= { ... }', ensuring all padding and reserved fields are zero-filled.\n5. Bug profile: The primary risk surface of the added element parsers, frame classifiers, and driver callbacks involves out-of-bounds array reads, parser boundary validation, concurrency/locking, or object lifetime management. These failure modes are comprehensively covered by KASAN, UBSAN, and LOCKDEP.\n\nBecause no uninitialized memory usage or info-leak vectors are introduced, a dedicated KMSAN fuzzing session is not justified.",
"NeedsKMSAN": false
} |
| Total Calls | Total Tokens | Avg Tokens | Total Duration (Seconds) | Avg Duration (Seconds) |
|---|
| Total Calls | Total Duration (Seconds) | Avg Duration (Seconds) |
|---|