From: Johannes Berg <[email protected]>

If the regulatory database is loaded, and then updated, it may
be necessary to reload it. Add an nl80211 command to do this,
and RCU-ify the pointer to the regdb "firmware" to allow it to
be replaced at runtime.

Signed-off-by: Johannes Berg <[email protected]>
---
 include/uapi/linux/nl80211.h |  4 +++
 net/wireless/nl80211.c       | 11 ++++++
 net/wireless/reg.c           | 84 ++++++++++++++++++++++++++++++++++++--------
 net/wireless/reg.h           |  6 ++++
 4 files changed, 90 insertions(+), 15 deletions(-)

diff --git a/include/uapi/linux/nl80211.h b/include/uapi/linux/nl80211.h
index 51626b4175c0..926eb92cb4a8 100644
--- a/include/uapi/linux/nl80211.h
+++ b/include/uapi/linux/nl80211.h
@@ -983,6 +983,8 @@
  *     configured PMK for the authenticator address identified by
  *     &NL80211_ATTR_MAC.
  *
+ * @NL80211_CMD_RELOAD_REGDB: Request that the regdb firmware file is reloaded.
+ *
  * @NL80211_CMD_MAX: highest used command number
  * @__NL80211_CMD_AFTER_LAST: internal use
  */
@@ -1185,6 +1187,8 @@ enum nl80211_commands {
        NL80211_CMD_SET_PMK,
        NL80211_CMD_DEL_PMK,
 
+       NL80211_CMD_RELOAD_REGDB,
+
        /* add new commands above here */
 
        /* used to define NL80211_CMD_MAX below */
diff --git a/net/wireless/nl80211.c b/net/wireless/nl80211.c
index 8ce85420ecb0..ee902ea13833 100644
--- a/net/wireless/nl80211.c
+++ b/net/wireless/nl80211.c
@@ -5669,6 +5669,11 @@ static int nl80211_req_set_reg(struct sk_buff *skb, 
struct genl_info *info)
        }
 }
 
+static int nl80211_reload_regdb(struct sk_buff *skb, struct genl_info *info)
+{
+       return reg_reload_regdb();
+}
+
 static int nl80211_get_mesh_config(struct sk_buff *skb,
                                   struct genl_info *info)
 {
@@ -12668,6 +12673,12 @@ static const struct genl_ops nl80211_ops[] = {
                .policy = nl80211_policy,
                .flags = GENL_ADMIN_PERM,
        },
+       {
+               .cmd = NL80211_CMD_RELOAD_REGDB,
+               .doit = nl80211_reload_regdb,
+               .policy = nl80211_policy,
+               .flags = GENL_ADMIN_PERM,
+       },
        {
                .cmd = NL80211_CMD_GET_MESH_CONFIG,
                .doit = nl80211_get_mesh_config,
diff --git a/net/wireless/reg.c b/net/wireless/reg.c
index 7a1d6cda64f3..8d366738c08a 100644
--- a/net/wireless/reg.c
+++ b/net/wireless/reg.c
@@ -602,7 +602,7 @@ static inline int call_crda(const char *alpha2)
 #endif /* CONFIG_CFG80211_CRDA_SUPPORT */
 
 /* code to directly load a firmware database through request_firmware */
-static const struct firmware *regdb;
+static const struct firmware __rcu *regdb;
 static unsigned int fwregdb_attempts = 10;
 
 struct fwdb_country {
@@ -756,47 +756,76 @@ static int query_regdb(const char *alpha2)
 {
        const struct fwdb_header *hdr;
        const struct fwdb_country *country;
+       const struct firmware *db;
+       int err;
 
-       if (IS_ERR(regdb))
-               return PTR_ERR(regdb);
+       rcu_read_lock();
+       db = rcu_dereference(regdb);
 
-       hdr = (void *)regdb->data;
+       if (IS_ERR(db)) {
+               err = PTR_ERR(db);
+               goto out;
+       }
+
+       hdr = (void *)db->data;
        country = &hdr->country[0];
        while (country->coll_ptr) {
-               if (alpha2_equal(alpha2, country->alpha2))
-                       return regdb_query_country(regdb, country);
+               if (alpha2_equal(alpha2, country->alpha2)) {
+                       err = regdb_query_country(db, country);
+                       goto out;
+               }
                country++;
        }
 
-       return -ENODATA;
+       err = -ENODATA;
+ out:
+       rcu_read_unlock();
+       return err;
 }
 
 static void regdb_fw_cb(const struct firmware *fw, void *context)
 {
+       const struct firmware *db;
+
+       rtnl_lock();
+
+       db = rcu_dereference_protected(regdb, lockdep_rtnl_is_held());
+
+       if (WARN_ON(db)) {
+               release_firmware(fw);
+               goto out;
+       }
+
        if (!fw) {
                pr_info("failed to load regulatory.db\n");
                if (fwregdb_attempts-- == 0)
-                       regdb = ERR_PTR(-ENODATA);
+                       rcu_assign_pointer(regdb, ERR_PTR(-ENODATA));
                goto restore;
        }
 
        if (!valid_regdb(fw)) {
                pr_info("loaded regulatory.db is malformed\n");
                release_firmware(fw);
-               regdb = ERR_PTR(-EINVAL);
+               rcu_assign_pointer(regdb, ERR_PTR(-EINVAL));
                goto restore;
        }
 
-       regdb = fw;
-       if (query_regdb(context))
+       rcu_assign_pointer(regdb, fw);
+
+       if (context && query_regdb(context))
                goto restore;
-       goto free;
+       goto out;
+
  restore:
-       rtnl_lock();
        restore_regulatory_settings(true);
+ out:
        rtnl_unlock();
- free:
        kfree(context);
+
+       if (!IS_ERR_OR_NULL(db)) {
+               synchronize_rcu();
+               release_firmware(db);
+       }
 }
 
 static int query_regdb_file(const char *alpha2)
@@ -813,6 +842,31 @@ static int query_regdb_file(const char *alpha2)
                                       (void *)alpha2, regdb_fw_cb);
 }
 
+int reg_reload_regdb(void)
+{
+       const struct firmware *db, *old;
+       int err;
+
+       err = request_firmware(&db, "regulatory.db", &reg_pdev->dev);
+       if (err)
+               return err;
+
+       if (!valid_regdb(db))
+               return -ENODATA;
+
+       rtnl_lock();
+       old = rcu_dereference_protected(regdb, lockdep_rtnl_is_held());
+       rcu_assign_pointer(regdb, db);
+       rtnl_unlock();
+
+       if (!IS_ERR_OR_NULL(old)) {
+               synchronize_rcu();
+               release_firmware(old);
+       }
+
+       return 0;
+}
+
 static bool reg_query_database(struct regulatory_request *request)
 {
        /* query internal regulatory database (if it exists) */
@@ -3564,5 +3618,5 @@ void regulatory_exit(void)
        }
 
        if (!IS_ERR_OR_NULL(regdb))
-               release_firmware(regdb);
+               release_firmware(rcu_access_pointer(regdb));
 }
diff --git a/net/wireless/reg.h b/net/wireless/reg.h
index ca7fedf2e7a1..9529c522611a 100644
--- a/net/wireless/reg.h
+++ b/net/wireless/reg.h
@@ -179,4 +179,10 @@ void regulatory_propagate_dfs_state(struct wiphy *wiphy,
  * @wiphy2 - wiphy it's dfs_region to be checked against that of wiphy1
  */
 bool reg_dfs_domain_same(struct wiphy *wiphy1, struct wiphy *wiphy2);
+
+/**
+ * reg_reload_regdb - reload the regulatory.db firmware file
+ */
+int reg_reload_regdb(void);
+
 #endif  /* __NET_WIRELESS_REG_H */
-- 
2.14.1

Reply via email to