nl80211_parse_sched_scan(struct wiphy *wiphy, struct wireless_dev *wdev,
			 struct nlattr **attrs, int max_match_sets)
{
	struct cfg80211_sched_scan_request *request;
	struct nlattr *attr;
	int err, tmp, n_ssids = 0, n_match_sets = 0, n_channels, i, n_plans = 0;
	enum nl80211_band band;
	size_t ie_len;
	struct nlattr *tb[NL80211_SCHED_SCAN_MATCH_ATTR_MAX + 1];
	s32 default_match_rssi = NL80211_SCAN_RSSI_THOLD_OFF;

	if (attrs[NL80211_ATTR_SCAN_FREQUENCIES]) {
		n_channels = validate_scan_freqs(
				attrs[NL80211_ATTR_SCAN_FREQUENCIES]);
		if (!n_channels)
			return ERR_PTR(-EINVAL);
	} else {
		n_channels = ieee80211_get_num_supported_channels(wiphy);
	}

	if (attrs[NL80211_ATTR_SCAN_SSIDS])
		nla_for_each_nested(attr, attrs[NL80211_ATTR_SCAN_SSIDS],
				    tmp)
			n_ssids++;

	if (n_ssids > wiphy->max_sched_scan_ssids)
		return ERR_PTR(-EINVAL);

	/*
	 * First, count the number of 'real' matchsets. Due to an issue with
	 * the old implementation, matchsets containing only the RSSI attribute
	 * (NL80211_SCHED_SCAN_MATCH_ATTR_RSSI) are considered as the 'default'
	 * RSSI for all matchsets, rather than their own matchset for reporting
	 * all APs with a strong RSSI. This is needed to be compatible with
	 * older userspace that treated a matchset with only the RSSI as the
	 * global RSSI for all other matchsets - if there are other matchsets.
	 */
	if (attrs[NL80211_ATTR_SCHED_SCAN_MATCH]) {
		nla_for_each_nested(attr,
				    attrs[NL80211_ATTR_SCHED_SCAN_MATCH],
				    tmp) {
			struct nlattr *rssi;

			err = nla_parse_nested_deprecated(tb,
							  NL80211_SCHED_SCAN_MATCH_ATTR_MAX,
							  attr,
							  nl80211_match_policy,
							  NULL);
			if (err)
				return ERR_PTR(err);

			/* SSID and BSSID are mutually exclusive */
			if (tb[NL80211_SCHED_SCAN_MATCH_ATTR_SSID] &&
			    tb[NL80211_SCHED_SCAN_MATCH_ATTR_BSSID])
				return ERR_PTR(-EINVAL);

			/* add other standalone attributes here */
			if (tb[NL80211_SCHED_SCAN_MATCH_ATTR_SSID] ||
			    tb[NL80211_SCHED_SCAN_MATCH_ATTR_BSSID]) {
				n_match_sets++;
				continue;
			}
			rssi = tb[NL80211_SCHED_SCAN_MATCH_ATTR_RSSI];
			if (rssi)
				default_match_rssi = nla_get_s32(rssi);
		}
	}

	/* However, if there's no other matchset, add the RSSI one */
	if (!n_match_sets && default_match_rssi != NL80211_SCAN_RSSI_THOLD_OFF)
		n_match_sets = 1;

	if (n_match_sets > max_match_sets)
		return ERR_PTR(-EINVAL);

	if (attrs[NL80211_ATTR_IE])
		ie_len = nla_len(attrs[NL80211_ATTR_IE]);
	else
		ie_len = 0;

	if (ie_len > wiphy->max_sched_scan_ie_len)
		return ERR_PTR(-EINVAL);

	if (attrs[NL80211_ATTR_SCHED_SCAN_PLANS]) {
		/*
		 * NL80211_ATTR_SCHED_SCAN_INTERVAL must not be specified since
		 * each scan plan already specifies its own interval
		 */
		if (attrs[NL80211_ATTR_SCHED_SCAN_INTERVAL])
			return ERR_PTR(-EINVAL);

		nla_for_each_nested(attr,
				    attrs[NL80211_ATTR_SCHED_SCAN_PLANS], tmp)
			n_plans++;
	} else {
		/*
		 * The scan interval attribute is kept for backward
		 * compatibility. If no scan plans are specified and sched scan
		 * interval is specified, one scan plan will be set with this
		 * scan interval and infinite number of iterations.
		 */
		if (!attrs[NL80211_ATTR_SCHED_SCAN_INTERVAL])
			return ERR_PTR(-EINVAL);

		n_plans = 1;
	}

	if (!n_plans || n_plans > wiphy->max_sched_scan_plans)
		return ERR_PTR(-EINVAL);

	if (!wiphy_ext_feature_isset(
		    wiphy, NL80211_EXT_FEATURE_SCHED_SCAN_RELATIVE_RSSI) &&
	    (attrs[NL80211_ATTR_SCHED_SCAN_RELATIVE_RSSI] ||
	     attrs[NL80211_ATTR_SCHED_SCAN_RSSI_ADJUST]))
		return ERR_PTR(-EINVAL);

	request = kzalloc(sizeof(*request)
			+ sizeof(*request->ssids) * n_ssids
			+ sizeof(*request->match_sets) * n_match_sets
			+ sizeof(*request->scan_plans) * n_plans
			+ sizeof(*request->channels) * n_channels
			+ ie_len, GFP_KERNEL);
	if (!request)
		return ERR_PTR(-ENOMEM);

	if (n_ssids)
		request->ssids = (void *)&request->channels[n_channels];
	request->n_ssids = n_ssids;
	if (ie_len) {
		if (n_ssids)
			request->ie = (void *)(request->ssids + n_ssids);
		else
			request->ie = (void *)(request->channels + n_channels);
	}

	if (n_match_sets) {
		if (request->ie)
			request->match_sets = (void *)(request->ie + ie_len);
		else if (n_ssids)
			request->match_sets =
				(void *)(request->ssids + n_ssids);
		else
			request->match_sets =
				(void *)(request->channels + n_channels);
	}
	request->n_match_sets = n_match_sets;

	if (n_match_sets)
		request->scan_plans = (void *)(request->match_sets +
					       n_match_sets);
	else if (request->ie)
		request->scan_plans = (void *)(request->ie + ie_len);
	else if (n_ssids)
		request->scan_plans = (void *)(request->ssids + n_ssids);
	else
		request->scan_plans = (void *)(request->channels + n_channels);

	request->n_scan_plans = n_plans;

	i = 0;
	if (attrs[NL80211_ATTR_SCAN_FREQUENCIES]) {
		/* user specified, bail out if channel not found */
		nla_for_each_nested(attr,
				    attrs[NL80211_ATTR_SCAN_FREQUENCIES],
				    tmp) {
			struct ieee80211_channel *chan;

			chan = ieee80211_get_channel(wiphy, nla_get_u32(attr));

			if (!chan) {
				err = -EINVAL;
				goto out_free;
			}

			/* ignore disabled channels */
			if (chan->flags & IEEE80211_CHAN_DISABLED)
				continue;

			request->channels[i] = chan;
			i++;
		}
	} else {
		/* all channels */
		for (band = 0; band < NUM_NL80211_BANDS; band++) {
			int j;

			if (!wiphy->bands[band])
				continue;
			for (j = 0; j < wiphy->bands[band]->n_channels; j++) {
				struct ieee80211_channel *chan;

				chan = &wiphy->bands[band]->channels[j];

				if (chan->flags & IEEE80211_CHAN_DISABLED)
					continue;

				request->channels[i] = chan;
				i++;
			}
		}
	}

	if (!i) {
		err = -EINVAL;
		goto out_free;
	}

	request->n_channels = i;

	i = 0;
	if (n_ssids) {
		nla_for_each_nested(attr, attrs[NL80211_ATTR_SCAN_SSIDS],
				    tmp) {
			if (nla_len(attr) > IEEE80211_MAX_SSID_LEN) {
				err = -EINVAL;
				goto out_free;
			}
			request->ssids[i].ssid_len = nla_len(attr);
			memcpy(request->ssids[i].ssid, nla_data(attr),
			       nla_len(attr));
			i++;
		}
	}

	i = 0;
	if (attrs[NL80211_ATTR_SCHED_SCAN_MATCH]) {
		nla_for_each_nested(attr,
				    attrs[NL80211_ATTR_SCHED_SCAN_MATCH],
				    tmp) {
			struct nlattr *ssid, *bssid, *rssi;

			err = nla_parse_nested_deprecated(tb,
							  NL80211_SCHED_SCAN_MATCH_ATTR_MAX,
							  attr,
							  nl80211_match_policy,
							  NULL);
			if (err)
				goto out_free;
			ssid = tb[NL80211_SCHED_SCAN_MATCH_ATTR_SSID];
			bssid = tb[NL80211_SCHED_SCAN_MATCH_ATTR_BSSID];

			if (!ssid && !bssid) {
				i++;
				continue;
			}

			if (WARN_ON(i >= n_match_sets)) {
				/* this indicates a programming error,
				 * the loop above should have verified
				 * things properly
				 */
				err = -EINVAL;
				goto out_free;
			}

			if (ssid) {
				if (nla_len(ssid) > IEEE80211_MAX_SSID_LEN) {
					err = -EINVAL;
					goto out_free;
				}
				memcpy(request->match_sets[i].ssid.ssid,
				       nla_data(ssid), nla_len(ssid));
				request->match_sets[i].ssid.ssid_len =
					nla_len(ssid);
			}
			if (bssid) {
				if (nla_len(bssid) != ETH_ALEN) {
					err = -EINVAL;
					goto out_free;
				}
				memcpy(request->match_sets[i].bssid,
				       nla_data(bssid), ETH_ALEN);
			}

			/* special attribute - old implementation w/a */
			request->match_sets[i].rssi_thold = default_match_rssi;
			rssi = tb[NL80211_SCHED_SCAN_MATCH_ATTR_RSSI];
			if (rssi)
				request->match_sets[i].rssi_thold =
					nla_get_s32(rssi);

			/* Parse per band RSSI attribute */
			err = nl80211_parse_sched_scan_per_band_rssi(wiphy,
				&request->match_sets[i],
				tb[NL80211_SCHED_SCAN_MATCH_PER_BAND_RSSI],
				request->match_sets[i].rssi_thold);
			if (err)
				goto out_free;

			i++;
		}

		/* there was no other matchset, so the RSSI one is alone */
		if (i == 0 && n_match_sets)
			request->match_sets[0].rssi_thold = default_match_rssi;

		request->min_rssi_thold = INT_MAX;
		for (i = 0; i < n_match_sets; i++)
			request->min_rssi_thold =
				min(request->match_sets[i].rssi_thold,
				    request->min_rssi_thold);
	} else {
		request->min_rssi_thold = NL80211_SCAN_RSSI_THOLD_OFF;
	}

	if (ie_len) {
		request->ie_len = ie_len;
		memcpy((void *)request->ie,
		       nla_data(attrs[NL80211_ATTR_IE]),
		       request->ie_len);
	}

	err = nl80211_check_scan_flags(wiphy, wdev, request, attrs, true);
	if (err)
		goto out_free;

	if (attrs[NL80211_ATTR_SCHED_SCAN_DELAY])
		request->delay =
			nla_get_u32(attrs[NL80211_ATTR_SCHED_SCAN_DELAY]);

	if (attrs[NL80211_ATTR_SCHED_SCAN_RELATIVE_RSSI]) {
		request->relative_rssi = nla_get_s8(
			attrs[NL80211_ATTR_SCHED_SCAN_RELATIVE_RSSI]);
		request->relative_rssi_set = true;
	}

	if (request->relative_rssi_set &&
	    attrs[NL80211_ATTR_SCHED_SCAN_RSSI_ADJUST]) {
		struct nl80211_bss_select_rssi_adjust *rssi_adjust;

		rssi_adjust = nla_data(
			attrs[NL80211_ATTR_SCHED_SCAN_RSSI_ADJUST]);
		request->rssi_adjust.band = rssi_adjust->band;
		request->rssi_adjust.delta = rssi_adjust->delta;
		if (!is_band_valid(wiphy, request->rssi_adjust.band)) {
			err = -EINVAL;
			goto out_free;
		}
	}

	err = nl80211_parse_sched_scan_plans(wiphy, n_plans, request, attrs);
	if (err)
		goto out_free;

	request->scan_start = jiffies;

	return request;

out_free:
	kfree(request);
	return ERR_PTR(err);
}