The struct_ops link can outlive the last policy using the ext governor. Switching that policy to performance releases the DBS tuners and clears ext_global_tuner. Destroying the link afterwards calls ext_disable(), which unconditionally dereferences the NULL pointer. Serialize tuner publication and release with BPF registration and unregistration using ext_mutex, and allow unregistering after the tuners have gone away. Preserve registered BPF operations when the governor is initialized again while the link is still alive. Take the CPU hotplug read lock before ext_mutex in the BPF callbacks and use the cpuslocked static-key helpers. Governor callbacks can run under the hotplug write lock, so acquiring the read lock inside ext_mutex would invert their lock order. Reproduce by selecting ext, attaching the RFC sample, selecting performance, then destroying the link. This was tested on arm64 QEMU with Linux 6.12 plus the RFC and Jinghao Zhou's ext_gov_update() fix. Lifecycle, frequency-limit and original sample checks also passed with lockdep, jump labels and CPU hotplug enabled. The 2025 fix addresses a separate policy-state lookup error. This teardown bug is present in the original, unmerged RFC as well. Link: https://lore.kernel.org/linux-pm/20240927101342.3240263-2-zouyipeng@huawei.com/ Link: https://lore.kernel.org/linux-pm/20250918125241.80968-1-zhoujinghao24b@gmail.com/ Signed-off-by: Hungyu Lin --- drivers/cpufreq/cpufreq_ext.c | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/drivers/cpufreq/cpufreq_ext.c b/drivers/cpufreq/cpufreq_ext.c index c79068e86..20d4d3648 100644 --- a/drivers/cpufreq/cpufreq_ext.c +++ b/drivers/cpufreq/cpufreq_ext.c @@ -197,8 +197,9 @@ static int ext_check_member(const struct btf_type *t, static void ext_disable(void) { pr_info("%s : Unregister ext governor(%s).\n", __func__, ext_ops_global.name); - static_branch_disable(&ext_gov_load); - ext_global_tuner->ext_gov_stat = CPUFREQ_EXT_INIT; + static_branch_disable_cpuslocked(&ext_gov_load); + if (ext_global_tuner) + ext_global_tuner->ext_gov_stat = CPUFREQ_EXT_INIT; ext_ops_global.get_next_freq = get_next_freq_nop; ext_ops_global.get_sampling_rate = get_sampling_rate_nop; ext_ops_global.init = init_nop; @@ -210,6 +211,7 @@ static int ext_reg(void *kdata, struct bpf_link *link) { struct cpufreq_governor_ext_ops *ops = (struct cpufreq_governor_ext_ops *)kdata; + cpus_read_lock(); mutex_lock(&ext_mutex); /* @@ -218,12 +220,14 @@ static int ext_reg(void *kdata, struct bpf_link *link) if (ext_global_tuner == NULL) { pr_err("%s : Need set ext governor first.\n", __func__); mutex_unlock(&ext_mutex); + cpus_read_unlock(); return -EEXIST; } if (ext_global_tuner->ext_gov_stat != CPUFREQ_EXT_INIT) { pr_err("%s : Already register.\n", __func__); mutex_unlock(&ext_mutex); + cpus_read_unlock(); return -EEXIST; } @@ -233,17 +237,20 @@ static int ext_reg(void *kdata, struct bpf_link *link) if (ext_ops_global.init && ext_ops_global.init()) { ext_disable(); mutex_unlock(&ext_mutex); + cpus_read_unlock(); return -EINVAL; } - static_branch_enable(&ext_gov_load); + static_branch_enable_cpuslocked(&ext_gov_load); pr_info("%s: Register ext governor(%s).\n", __func__, ext_ops_global.name); mutex_unlock(&ext_mutex); + cpus_read_unlock(); return 0; } static void ext_unreg(void *kdata, struct bpf_link *link) { + cpus_read_lock(); mutex_lock(&ext_mutex); if (ext_ops_global.exit) @@ -251,6 +258,7 @@ static void ext_unreg(void *kdata, struct bpf_link *link) ext_disable(); mutex_unlock(&ext_mutex); + cpus_read_unlock(); } static int ext_init(struct btf *btf) @@ -470,9 +478,13 @@ static int ext_gov_init(struct dbs_data *dbs_data) dbs_data->ignore_nice_load = 0; dbs_data->tuners = tuner; dbs_data->up_threshold = DEAFULT_FREQUENCY_UP_THRESHOLD; + + mutex_lock(&ext_mutex); ext_global_tuner = tuner; - /* Disable ext_gov_load as default */ - static_branch_disable(&ext_gov_load); + if (static_branch_unlikely(&ext_gov_load)) { + tuner->ext_gov_stat = CPUFREQ_EXT_LOADED; + goto unlock; + } /* As default, set ext_ops_global to nop function */ ext_ops_global.get_next_freq = get_next_freq_nop; @@ -480,6 +492,8 @@ static int ext_gov_init(struct dbs_data *dbs_data) ext_ops_global.init = init_nop; ext_ops_global.exit = exit_nop; strscpy(ext_ops_global.name, CPUFREQ_EXT_DEFAULT_NAME, strlen(CPUFREQ_EXT_DEFAULT_NAME)); +unlock: + mutex_unlock(&ext_mutex); return 0; } @@ -490,11 +504,14 @@ static void ext_gov_exit(struct dbs_data *dbs_data) if (!dbs_data || !dbs_data->tuners) return; + mutex_lock(&ext_mutex); tuner = (struct ext_tuner *)dbs_data->tuners; tuner->ext_gov_stat = CPUFREQ_EXT_NONE; + if (ext_global_tuner == tuner) + ext_global_tuner = NULL; kfree(dbs_data->tuners); dbs_data->tuners = NULL; - ext_global_tuner = NULL; + mutex_unlock(&ext_mutex); } static void ext_gov_start(struct cpufreq_policy *policy) -- 2.43.0