root/drivers/net/wireless/intel/iwlwifi/mld/mcc.c
// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
/*
 * Copyright (C) 2024-2025 Intel Corporation
 */

#include <net/cfg80211.h>
#include <net/mac80211.h>

#include <fw/dbg.h>
#include <iwl-nvm-parse.h>

#include "mld.h"
#include "hcmd.h"
#include "mcc.h"

/* It is the caller's responsibility to free the pointer returned here */
static struct iwl_mcc_update_resp_v8 *
iwl_mld_copy_mcc_resp(const struct iwl_rx_packet *pkt)
{
        const struct iwl_mcc_update_resp_v8 *mcc_resp_v8 = (const void *)pkt->data;
        int n_channels = __le32_to_cpu(mcc_resp_v8->n_channels);
        struct iwl_mcc_update_resp_v8 *resp_cp;
        int notif_len = struct_size(resp_cp, channels, n_channels);

        if (iwl_rx_packet_payload_len(pkt) != notif_len)
                return ERR_PTR(-EINVAL);

        resp_cp = kmemdup(mcc_resp_v8, notif_len, GFP_KERNEL);
        if (!resp_cp)
                return ERR_PTR(-ENOMEM);

        return resp_cp;
}

/* It is the caller's responsibility to free the pointer returned here */
static struct iwl_mcc_update_resp_v8 *
iwl_mld_update_mcc(struct iwl_mld *mld, const char *alpha2,
                   enum iwl_mcc_source src_id)
{
        struct iwl_mcc_update_cmd mcc_update_cmd = {
                .mcc = cpu_to_le16(alpha2[0] << 8 | alpha2[1]),
                .source_id = (u8)src_id,
        };
        struct iwl_mcc_update_resp_v8 *resp_cp;
        struct iwl_rx_packet *pkt;
        struct iwl_host_cmd cmd = {
                .id = MCC_UPDATE_CMD,
                .flags = CMD_WANT_SKB,
                .data = { &mcc_update_cmd },
                .len[0] = sizeof(mcc_update_cmd),
        };
        int ret;
        u16 mcc;

        IWL_DEBUG_LAR(mld, "send MCC update to FW with '%c%c' src = %d\n",
                      alpha2[0], alpha2[1], src_id);

        ret = iwl_mld_send_cmd(mld, &cmd);
        if (ret)
                return ERR_PTR(ret);

        pkt = cmd.resp_pkt;

        resp_cp = iwl_mld_copy_mcc_resp(pkt);
        if (IS_ERR(resp_cp))
                goto exit;

        mcc = le16_to_cpu(resp_cp->mcc);

        IWL_FW_CHECK(mld, !mcc, "mcc can't be 0: %d\n", mcc);

        IWL_DEBUG_LAR(mld,
                      "MCC response status: 0x%x. new MCC: 0x%x ('%c%c')\n",
                      le32_to_cpu(resp_cp->status), mcc, mcc >> 8, mcc & 0xff);

exit:
        iwl_free_resp(&cmd);
        return resp_cp;
}

/* It is the caller's responsibility to free the pointer returned here */
struct ieee80211_regdomain *
iwl_mld_get_regdomain(struct iwl_mld *mld,
                      const char *alpha2,
                      enum iwl_mcc_source src_id,
                      bool *changed)
{
        struct ieee80211_regdomain *regd = NULL;
        struct iwl_mcc_update_resp_v8 *resp;
        u8 resp_ver = iwl_fw_lookup_notif_ver(mld->fw, IWL_ALWAYS_LONG_GROUP,
                                              MCC_UPDATE_CMD, 0);

        IWL_DEBUG_LAR(mld, "Getting regdomain data for %s from FW\n", alpha2);

        lockdep_assert_wiphy(mld->wiphy);

        resp = iwl_mld_update_mcc(mld, alpha2, src_id);
        if (IS_ERR(resp)) {
                IWL_DEBUG_LAR(mld, "Could not get update from FW %ld\n",
                              PTR_ERR(resp));
                resp = NULL;
                goto out;
        }

        if (changed) {
                u32 status = le32_to_cpu(resp->status);

                *changed = (status == MCC_RESP_NEW_CHAN_PROFILE ||
                            status == MCC_RESP_ILLEGAL);
        }
        IWL_DEBUG_LAR(mld, "MCC update response version: %d\n", resp_ver);

        regd = iwl_parse_nvm_mcc_info(mld->trans,
                                      __le32_to_cpu(resp->n_channels),
                                      resp->channels,
                                      __le16_to_cpu(resp->mcc),
                                      __le16_to_cpu(resp->geo_info),
                                      le32_to_cpu(resp->cap), resp_ver);

        if (IS_ERR(regd)) {
                IWL_DEBUG_LAR(mld, "Could not get parse update from FW %ld\n",
                              PTR_ERR(regd));
                goto out;
        }

        IWL_DEBUG_LAR(mld, "setting alpha2 from FW to %s (0x%x, 0x%x) src=%d\n",
                      regd->alpha2, regd->alpha2[0],
                      regd->alpha2[1], resp->source_id);

        mld->mcc_src = resp->source_id;

        /* FM is the earliest supported and later always do puncturing */
        if (CSR_HW_RFID_TYPE(mld->trans->info.hw_rf_id) == IWL_CFG_RF_TYPE_FM) {
                if (!iwl_puncturing_is_allowed_in_bios(mld->bios_enable_puncturing,
                                                       le16_to_cpu(resp->mcc)))
                        ieee80211_hw_set(mld->hw, DISALLOW_PUNCTURING);
                else
                        __clear_bit(IEEE80211_HW_DISALLOW_PUNCTURING,
                                    mld->hw->flags);
        }

out:
        kfree(resp);
        return regd;
}

/* It is the caller's responsibility to free the pointer returned here */
static struct ieee80211_regdomain *
iwl_mld_get_current_regdomain(struct iwl_mld *mld,
                              bool *changed)
{
        return iwl_mld_get_regdomain(mld, "ZZ",
                                     MCC_SOURCE_GET_CURRENT, changed);
}

void iwl_mld_update_changed_regdomain(struct iwl_mld *mld)
{
        struct ieee80211_regdomain *regd;
        bool changed;

        regd = iwl_mld_get_current_regdomain(mld, &changed);

        if (IS_ERR_OR_NULL(regd))
                return;

        if (changed)
                regulatory_set_wiphy_regd(mld->wiphy, regd);
        kfree(regd);
}

static int iwl_mld_apply_last_mcc(struct iwl_mld *mld,
                                  const char *alpha2)
{
        struct ieee80211_regdomain *regd;
        u32 used_src;
        bool changed;
        int ret;

        /* save the last source in case we overwrite it below */
        used_src = mld->mcc_src;

        /* Notify the firmware we support wifi location updates */
        regd = iwl_mld_get_current_regdomain(mld, NULL);
        if (!IS_ERR_OR_NULL(regd))
                kfree(regd);

        /* Now set our last stored MCC and source */
        regd = iwl_mld_get_regdomain(mld, alpha2, used_src,
                                     &changed);
        if (IS_ERR_OR_NULL(regd))
                return -EIO;

        /* update cfg80211 if the regdomain was changed */
        if (changed)
                ret = regulatory_set_wiphy_regd_sync(mld->wiphy, regd);
        else
                ret = 0;

        kfree(regd);
        return ret;
}

int iwl_mld_init_mcc(struct iwl_mld *mld)
{
        const struct ieee80211_regdomain *r;
        struct ieee80211_regdomain *regd;
        char mcc[3];
        int retval;

        /* try to replay the last set MCC to FW */
        r = wiphy_dereference(mld->wiphy, mld->wiphy->regd);

        if (r)
                return iwl_mld_apply_last_mcc(mld, r->alpha2);

        regd = iwl_mld_get_current_regdomain(mld, NULL);
        if (IS_ERR_OR_NULL(regd))
                return -EIO;

        if (!iwl_bios_get_mcc(&mld->fwrt, mcc)) {
                kfree(regd);
                regd = iwl_mld_get_regdomain(mld, mcc, MCC_SOURCE_BIOS, NULL);
                if (IS_ERR_OR_NULL(regd))
                        return -EIO;
        }

        retval = regulatory_set_wiphy_regd_sync(mld->wiphy, regd);

        kfree(regd);
        return retval;
}

static void iwl_mld_find_assoc_vif_iterator(void *data, u8 *mac,
                                            struct ieee80211_vif *vif)
{
        bool *assoc = data;

        if (vif->type == NL80211_IFTYPE_STATION &&
            vif->cfg.assoc)
                *assoc = true;
}

static bool iwl_mld_is_a_vif_assoc(struct iwl_mld *mld)
{
        bool assoc = false;

        ieee80211_iterate_active_interfaces_atomic(mld->hw,
                                                   IEEE80211_IFACE_ITER_NORMAL,
                                                   iwl_mld_find_assoc_vif_iterator,
                                                   &assoc);
        return assoc;
}

void iwl_mld_handle_update_mcc(struct iwl_mld *mld, struct iwl_rx_packet *pkt)
{
        struct iwl_mcc_chub_notif *notif = (void *)pkt->data;
        enum iwl_mcc_source src;
        char mcc[3];
        struct ieee80211_regdomain *regd;
        bool changed;

        lockdep_assert_wiphy(mld->wiphy);

        if (iwl_mld_is_a_vif_assoc(mld) &&
            notif->source_id == MCC_SOURCE_WIFI) {
                IWL_DEBUG_LAR(mld, "Ignore mcc update while associated\n");
                return;
        }

        mcc[0] = le16_to_cpu(notif->mcc) >> 8;
        mcc[1] = le16_to_cpu(notif->mcc) & 0xff;
        mcc[2] = '\0';
        src = notif->source_id;

        IWL_DEBUG_LAR(mld,
                      "RX: received chub update mcc cmd (mcc '%s' src %d)\n",
                      mcc, src);
        regd = iwl_mld_get_regdomain(mld, mcc, src, &changed);
        if (IS_ERR_OR_NULL(regd))
                return;

        if (changed)
                regulatory_set_wiphy_regd(mld->hw->wiphy, regd);
        kfree(regd);
}