diff --git a/net/mac80211/cfg.c b/net/mac80211/cfg.c
index ff090ef1ea2c..23a104ebea93 100644
--- a/net/mac80211/cfg.c
+++ b/net/mac80211/cfg.c
@@ -3690,6 +3690,46 @@ static int ieee80211_del_tx_ts(struct wiphy *wiphy, struct net_device *dev,
 	return -ENOENT;
 }
 
+static void ieee80211_rate_stats(struct wiphy *wiphy,
+				 enum cfg80211_rate_stats_ops op)
+{
+	struct ieee80211_local *local = wiphy_priv(wiphy);
+	struct sta_info *sta;
+	struct ieee80211_sta_rate_stats *stats, *tmp;
+	LIST_HEAD(list);
+
+	mutex_lock(&local->sta_mtx);
+	switch (op) {
+	case CFG80211_RATE_STATS_START:
+		local->rate_stats_active = true;
+		list_for_each_entry(sta, &local->sta_list, list)
+			ieee80211_sta_start_rate_stats(sta);
+		/* list stays empty */
+		break;
+	case CFG80211_RATE_STATS_DUMP:
+		list_for_each_entry(sta, &local->sta_list, list) {
+			stats = ieee80211_sta_reset_rate_stats(sta);
+			if (stats)
+				list_add_tail(&stats->list, &list);
+		}
+		break;
+	case CFG80211_RATE_STATS_STOP:
+		local->rate_stats_active = false;
+		list_for_each_entry(sta, &local->sta_list, list) {
+			stats = ieee80211_sta_stop_rate_stats(sta);
+			if (stats)
+				list_add_tail(&stats->list, &list);
+		}
+		break;
+	}
+	mutex_unlock(&local->sta_mtx);
+
+	if (!list_empty(&list))
+		synchronize_rcu();
+	list_for_each_entry_safe(stats, tmp, &list, list)
+		ieee80211_sta_free_rate_stats(stats, true);
+}
+
 const struct cfg80211_ops mac80211_config_ops = {
 	.add_virtual_intf = ieee80211_add_iface,
 	.del_virtual_intf = ieee80211_del_iface,
@@ -3774,4 +3814,5 @@ const struct cfg80211_ops mac80211_config_ops = {
 	.set_ap_chanwidth = ieee80211_set_ap_chanwidth,
 	.add_tx_ts = ieee80211_add_tx_ts,
 	.del_tx_ts = ieee80211_del_tx_ts,
+	.rate_stats = ieee80211_rate_stats,
 };
diff --git a/net/mac80211/ieee80211_i.h b/net/mac80211/ieee80211_i.h
index 156ea79e0157..59f1b2ecad16 100644
--- a/net/mac80211/ieee80211_i.h
+++ b/net/mac80211/ieee80211_i.h
@@ -1126,6 +1126,8 @@ struct ieee80211_local {
 
 	bool use_chanctx;
 
+	bool rate_stats_active;
+
 	/* protects the aggregated multicast list and filter calls */
 	spinlock_t filter_lock;
 
diff --git a/net/mac80211/main.c b/net/mac80211/main.c
index d9ce33663c73..d26fe28e07f2 100644
--- a/net/mac80211/main.c
+++ b/net/mac80211/main.c
@@ -545,6 +545,8 @@ struct ieee80211_hw *ieee80211_alloc_hw_nm(size_t priv_data_len,
 			   NL80211_FEATURE_MAC_ON_CREATE |
 			   NL80211_FEATURE_USERSPACE_MPM;
 
+	wiphy_ext_feature_set(wiphy, NL80211_EXT_FEATURE_RATE_STATS);
+
 	if (!ops->hw_scan)
 		wiphy->features |= NL80211_FEATURE_LOW_PRIORITY_SCAN |
 				   NL80211_FEATURE_AP_SCAN;
@@ -859,6 +861,16 @@ int ieee80211_register_hw(struct ieee80211_hw *hw)
 		/* TODO: consider VHT for RX chains, hopefully it's the same */
 	}
 
+	/* if the HW has more than 12 legacy rates, some assumptions in the
+	 * data structures break - in this case don't allow the rate-stats
+	 * feature flag.
+	 */
+	if (WARN_ON(wiphy_ext_feature_isset(hw->wiphy,
+					    NL80211_EXT_FEATURE_RATE_STATS) &&
+		    max_bitrates > 12))
+		hw->wiphy->ext_features[NL80211_EXT_FEATURE_RATE_STATS / 8] &=
+			~BIT(NL80211_EXT_FEATURE_RATE_STATS % 8);
+
 	/* if low-level driver supports AP, we also support VLAN */
 	if (local->hw.wiphy->interface_modes & BIT(NL80211_IFTYPE_AP)) {
 		hw->wiphy->interface_modes |= BIT(NL80211_IFTYPE_AP_VLAN);
diff --git a/net/mac80211/rx.c b/net/mac80211/rx.c
index 3d79d498e7f6..8188ecdeaa8d 100644
--- a/net/mac80211/rx.c
+++ b/net/mac80211/rx.c
@@ -2292,6 +2292,56 @@ ieee80211_rx_h_mesh_fwding(struct ieee80211_rx_data *rx)
 }
 #endif
 
+static void noinline
+ieee80211_rx_handle_rate_stats(struct ieee80211_local *local,
+			       struct ieee80211_rx_status *status,
+			       struct ieee80211_sta_rate_stats *rstats)
+{
+	u16 rate = sta_rate_stats_encode_rate(status);
+	unsigned int start;
+	int i;
+
+	start = status->rate_idx;
+	/* for HT - use only the base index */
+	if (status->flag & RX_FLAG_HT)
+		start &= 0xF;
+	for (i = start; i < start + 3; i++) {
+		if (rstats->entries[i].rate == rate ||
+		    rstats->entries[i].rate == STA_RATE_STATS_RATE_INVALID) {
+			if (rstats->entries[i].rate ==
+					STA_RATE_STATS_RATE_INVALID)
+				rstats->entries[i].rate = rate;
+			if (rstats->entries[i].rx++ > 64000)
+				goto schedule_dump;
+			return;
+		}
+	}
+
+	/* The last six entries are only really used by legacy rates - so
+	 * not that performance sensitive.  For HT and VHT though, only the
+	 * first 24 (3*8) or 30 (3*10) entries can be used, so try the last
+	 * few in the list as "escape" entries. If any such entry is used
+	 * then schedule dumping to userspace unconditionally to avoid going
+	 * into this code path frequently.
+	 */
+	for (i = ARRAY_SIZE(rstats->entries) - 1;
+	     i > ARRAY_SIZE(rstats->entries) - 7; i--) {
+		if (rstats->entries[i].rate == rate ||
+		    rstats->entries[i].rate == STA_RATE_STATS_RATE_INVALID) {
+			if (rstats->entries[i].rate ==
+					STA_RATE_STATS_RATE_INVALID)
+				rstats->entries[i].rate = rate;
+			rstats->entries[i].rx++;
+			break;
+		}
+	}
+
+	/* if we still didn't find anything, drop the stats for this packet */
+
+ schedule_dump:
+	ieee80211_queue_work(&local->hw, &rstats->dump_wk);
+}
+
 static ieee80211_rx_result debug_noinline
 ieee80211_rx_h_data(struct ieee80211_rx_data *rx)
 {
@@ -2310,12 +2360,19 @@ ieee80211_rx_h_data(struct ieee80211_rx_data *rx)
 		return RX_DROP_MONITOR;
 
 	if (rx->sta) {
+		struct ieee80211_sta_rate_stats *rstats;
+
 		/* The security index has the same property as needed
 		 * for the rx_msdu field, i.e. it is IEEE80211_NUM_TIDS
 		 * for non-QoS-data frames. Here we know it's a data
 		 * frame, so count MSDUs.
 		 */
 		rx->sta->rx_msdu[rx->security_idx]++;
+
+		rstats = rcu_dereference(rx->sta->rate_stats);
+		if (rstats)
+			ieee80211_rx_handle_rate_stats(local,
+				IEEE80211_SKB_RXCB(rx->skb), rstats);
 	}
 
 	/*
diff --git a/net/mac80211/sta_info.c b/net/mac80211/sta_info.c
index 79383ef0c264..2ca35425d9ab 100644
--- a/net/mac80211/sta_info.c
+++ b/net/mac80211/sta_info.c
@@ -497,6 +497,9 @@ static int sta_info_insert_finish(struct sta_info *sta) __acquires(RCU)
 	local->sta_generation++;
 	smp_mb();
 
+	if (local->rate_stats_active)
+		ieee80211_sta_start_rate_stats(sta);
+
 	/* simplify things and don't accept BA sessions yet */
 	set_sta_flag(sta, WLAN_STA_BLOCK_BA);
 
@@ -882,6 +885,7 @@ static void __sta_info_destroy_part2(struct sta_info *sta)
 	struct ieee80211_local *local = sta->local;
 	struct ieee80211_sub_if_data *sdata = sta->sdata;
 	struct station_info sinfo = {};
+	struct ieee80211_sta_rate_stats *stats;
 	int ret;
 
 	/*
@@ -919,6 +923,10 @@ static void __sta_info_destroy_part2(struct sta_info *sta)
 
 	sta_dbg(sdata, "Removed STA %pM\n", sta->sta.addr);
 
+	stats = ieee80211_sta_stop_rate_stats(sta);
+	/* station was already unlinked and rcu sync'ed before getting here */
+	ieee80211_sta_free_rate_stats(stats, true);
+
 	sta_set_sinfo(sta, &sinfo);
 	cfg80211_del_sta_sinfo(sdata->dev, sta->sta.addr, &sinfo, GFP_KERNEL);
 
@@ -1950,3 +1958,130 @@ void sta_set_sinfo(struct sta_info *sta, struct station_info *sinfo)
 		sinfo->expected_throughput = thr;
 	}
 }
+
+static void sta_rate_stats_decode_rate(u16 r, struct ieee80211_local *local,
+				       struct rate_info *rinfo)
+{
+	rinfo->bw = (r & STA_RATE_STATS_RATE_BW_MASK) >>
+		STA_RATE_STATS_RATE_BW_SHIFT;
+
+	if (r & STA_RATE_STATS_RATE_VHT) {
+		rinfo->flags = RATE_INFO_FLAGS_VHT_MCS;
+		rinfo->mcs = r & 0xf;
+		rinfo->nss = (r & 0xf0) >> 4;
+	} else if (r & STA_RATE_STATS_RATE_HT) {
+		rinfo->flags = RATE_INFO_FLAGS_MCS;
+		rinfo->mcs = r & 0xff;
+	} else if (r & STA_RATE_STATS_RATE_LEGACY) {
+		struct ieee80211_supported_band *sband;
+		u16 brate;
+		unsigned int shift;
+
+		sband = local->hw.wiphy->bands[(r >> 4) & 0xf];
+		brate = sband->bitrates[r & 0xf].bitrate;
+		if (rinfo->bw == RATE_INFO_BW_5)
+			shift = 2;
+		else if (rinfo->bw == RATE_INFO_BW_10)
+			shift = 1;
+		else
+			shift = 0;
+		rinfo->legacy = DIV_ROUND_UP(brate, 1 << shift);
+	}
+
+	if (r & STA_RATE_STATS_RATE_SGI)
+		rinfo->flags |= RATE_INFO_FLAGS_SHORT_GI;
+}
+
+void ieee80211_sta_start_rate_stats(struct sta_info *sta)
+{
+	struct ieee80211_sta_rate_stats *stats;
+
+	stats = ieee80211_sta_reset_rate_stats(sta);
+
+	if (WARN_ON_ONCE(stats)) {
+		/* error case - be really slow but correct */
+		synchronize_rcu();
+		ieee80211_sta_free_rate_stats(stats, true);
+	}
+}
+
+static void ieee80211_sta_dump_rate_stats_wk(struct work_struct *wk)
+{
+	struct ieee80211_sta_rate_stats *stats, *stats2;
+
+	stats = container_of(wk, struct ieee80211_sta_rate_stats, dump_wk);
+
+	mutex_lock(&stats->sta->local->sta_mtx);
+	stats2 = ieee80211_sta_reset_rate_stats(stats->sta);
+	mutex_unlock(&stats->sta->local->sta_mtx);
+
+	WARN_ON_ONCE(stats != stats2);
+
+	synchronize_rcu();
+	ieee80211_sta_free_rate_stats(stats, false);
+}
+
+struct ieee80211_sta_rate_stats * __must_check
+ieee80211_sta_reset_rate_stats(struct sta_info *sta)
+{
+	struct ieee80211_sta_rate_stats *stats, *new;
+
+	stats = rcu_dereference_protected(sta->rate_stats,
+			lockdep_is_held(&sta->local->sta_mtx));
+
+	new = kzalloc(sizeof(*new), GFP_KERNEL);
+	new->sta = sta;
+	INIT_WORK(&new->dump_wk, ieee80211_sta_dump_rate_stats_wk);
+
+	rcu_assign_pointer(sta->rate_stats, new);
+
+	/* caller must wait for grace period and free data */
+	return stats;
+}
+
+struct ieee80211_sta_rate_stats * __must_check
+ieee80211_sta_stop_rate_stats(struct sta_info *sta)
+{
+	struct ieee80211_sta_rate_stats *stats;
+
+	stats = rcu_dereference_protected(sta->rate_stats,
+			lockdep_is_held(&sta->local->sta_mtx));
+
+	RCU_INIT_POINTER(sta->rate_stats, NULL);
+
+	/* caller must wait for grace period and free data */
+	return stats;
+}
+
+void ieee80211_sta_free_rate_stats(struct ieee80211_sta_rate_stats *stats,
+				   bool flush)
+{
+	struct cfg80211_rate_stats report;
+	struct sta_info *sta;
+	int i;
+
+	if (!stats)
+		return;
+
+	if (flush && flush_work(&stats->dump_wk))
+		return;
+
+	sta = stats->sta;
+
+	for (i = 0; i < ARRAY_SIZE(stats->entries); i++) {
+		if (stats->entries[i].rate == STA_RATE_STATS_RATE_INVALID)
+			continue;
+
+		sta_rate_stats_decode_rate(stats->entries[i].rate,
+					   stats->sta->local, &report.rate);
+
+		report.stats.filled = BIT(NL80211_TID_STATS_RX_MSDU);
+		report.stats.rx_msdu = stats->entries[i].rx;
+
+		cfg80211_report_rate_stats(sta->local->hw.wiphy,
+					   &sta->sdata->wdev, sta->sta.addr,
+					   1, &report, GFP_KERNEL);
+	}
+
+	kfree(stats);
+}
diff --git a/net/mac80211/sta_info.h b/net/mac80211/sta_info.h
index 925e68fe64c7..cd7c7b5111dd 100644
--- a/net/mac80211/sta_info.h
+++ b/net/mac80211/sta_info.h
@@ -354,6 +354,7 @@ struct ieee80211_tx_latency_stat {
  *	using IEEE80211_NUM_TID entry for non-QoS frames
  * @rx_msdu: MSDUs received from this station, using IEEE80211_NUM_TID
  *	entry for non-QoS frames
+ * @rate_stats: rate statistics pointer (if enabled)
  */
 struct sta_info {
 	/* General information, mostly static */
@@ -428,6 +429,7 @@ struct sta_info {
 	u64 tx_msdu_retries[IEEE80211_NUM_TIDS + 1];
 	u64 tx_msdu_failed[IEEE80211_NUM_TIDS + 1];
 	u64 rx_msdu[IEEE80211_NUM_TIDS + 1];
+	struct ieee80211_sta_rate_stats __rcu *rate_stats;
 
 	/*
 	 * Aggregation information, locked with lock.
@@ -670,4 +672,61 @@ void ieee80211_sta_ps_deliver_wakeup(struct sta_info *sta);
 void ieee80211_sta_ps_deliver_poll_response(struct sta_info *sta);
 void ieee80211_sta_ps_deliver_uapsd(struct sta_info *sta);
 
+/* rate statistics */
+void ieee80211_sta_start_rate_stats(struct sta_info *sta);
+struct ieee80211_sta_rate_stats * __must_check
+ieee80211_sta_reset_rate_stats(struct sta_info *sta);
+struct ieee80211_sta_rate_stats * __must_check
+ieee80211_sta_stop_rate_stats(struct sta_info *sta);
+void ieee80211_sta_free_rate_stats(struct ieee80211_sta_rate_stats *stats,
+				   bool flush);
+
+struct ieee80211_sta_rate_stats {
+	struct list_head list;
+	struct sta_info *sta;
+	struct work_struct dump_wk;
+	struct {
+#define STA_RATE_STATS_RATE_INVALID	0
+#define STA_RATE_STATS_RATE_VHT		0x8000
+#define STA_RATE_STATS_RATE_HT		0x4000
+#define STA_RATE_STATS_RATE_LEGACY	0x2000
+#define STA_RATE_STATS_RATE_SGI		0x1000
+#define STA_RATE_STATS_RATE_BW_SHIFT	9
+#define STA_RATE_STATS_RATE_BW_MASK	(0x7 << STA_RATE_STATS_RATE_BW_SHIFT)
+		u16 rate;
+		u16 rx;
+	/* 3 * rate_idx/MCS - using a few from the end as escape */
+	} entries[36];
+};
+
+static inline u16 sta_rate_stats_encode_rate(struct ieee80211_rx_status *s)
+{
+	u16 r = s->rate_idx;
+
+	if (s->vht_flag & RX_VHT_FLAG_80MHZ)
+		r |= RATE_INFO_BW_80 << STA_RATE_STATS_RATE_BW_SHIFT;
+	else if (s->vht_flag & RX_VHT_FLAG_160MHZ)
+		r |= RATE_INFO_BW_160 << STA_RATE_STATS_RATE_BW_SHIFT;
+	else if (s->flag & RX_FLAG_40MHZ)
+		r |= RATE_INFO_BW_40 << STA_RATE_STATS_RATE_BW_SHIFT;
+	else if (s->flag & RX_FLAG_10MHZ)
+		r |= RATE_INFO_BW_10 << STA_RATE_STATS_RATE_BW_SHIFT;
+	else if (s->flag & RX_FLAG_5MHZ)
+		r |= RATE_INFO_BW_5 << STA_RATE_STATS_RATE_BW_SHIFT;
+	else
+		r |= RATE_INFO_BW_20 << STA_RATE_STATS_RATE_BW_SHIFT;
+
+	if (s->flag & RX_FLAG_SHORT_GI)
+		r |= STA_RATE_STATS_RATE_SGI;
+
+	if (s->flag & RX_FLAG_VHT)
+		r |= STA_RATE_STATS_RATE_VHT | (s->vht_nss << 4);
+	else if (s->flag & RX_FLAG_HT)
+		r |= STA_RATE_STATS_RATE_HT;
+	else
+		r |= STA_RATE_STATS_RATE_LEGACY | (s->band << 4);
+
+	return r;
+}
+
 #endif /* STA_INFO_H */
