hwmon: (pmbus/core) Protect regulator operations with mutex

The regulator operations pmbus_regulator_get_voltage(),
pmbus_regulator_set_voltage(), and pmbus_regulator_list_voltage()
access PMBus registers and shared data but were not protected by
the update_lock mutex. This could lead to race conditions.

However, adding mutex protection directly to these functions causes
a deadlock because pmbus_regulator_notify() (which calls
regulator_notifier_call_chain()) is often called with the mutex
already held (e.g., from pmbus_fault_handler()). If a regulator
callback then calls one of the now-protected voltage functions,
it will attempt to acquire the same mutex.

Rework pmbus_regulator_notify() to utilize a worker function to
send notifications outside of the mutex protection. Events are
stored as atomics in a per-page bitmask and processed by the worker.

Initialize the worker and its associated data during regulator
registration, and ensure it is cancelled on device removal using
devm_add_action_or_reset().

While at it, remove the unnecessary include of linux/of.h.

Cc: Sanman Pradhan <psanman@juniper.net>
Fixes: ddbb4db4ce ("hwmon: (pmbus) Add regulator support")
Reviewed-by: Sanman Pradhan <psanman@juniper.net>
Signed-off-by: Guenter Roeck <linux@roeck-us.net>
This commit is contained in:
Guenter Roeck 2026-03-22 09:12:33 -07:00
parent cd658475e7
commit 754bd2b4a0
1 changed files with 89 additions and 25 deletions

View File

@ -6,6 +6,7 @@
* Copyright (c) 2012 Guenter Roeck
*/
#include <linux/atomic.h>
#include <linux/debugfs.h>
#include <linux/delay.h>
#include <linux/dcache.h>
@ -21,8 +22,8 @@
#include <linux/pmbus.h>
#include <linux/regulator/driver.h>
#include <linux/regulator/machine.h>
#include <linux/of.h>
#include <linux/thermal.h>
#include <linux/workqueue.h>
#include "pmbus.h"
/*
@ -112,6 +113,11 @@ struct pmbus_data {
struct mutex update_lock;
#if IS_ENABLED(CONFIG_REGULATOR)
atomic_t regulator_events[PMBUS_PAGES];
struct work_struct regulator_notify_work;
#endif
bool has_status_word; /* device uses STATUS_WORD register */
int (*read_status)(struct i2c_client *client, int page);
@ -3228,12 +3234,19 @@ static int pmbus_regulator_get_voltage(struct regulator_dev *rdev)
.class = PSC_VOLTAGE_OUT,
.convert = true,
};
int ret;
mutex_lock(&data->update_lock);
s.data = _pmbus_read_word_data(client, s.page, 0xff, PMBUS_READ_VOUT);
if (s.data < 0)
return s.data;
if (s.data < 0) {
ret = s.data;
goto unlock;
}
return (int)pmbus_reg2data(data, &s) * 1000; /* unit is uV */
ret = (int)pmbus_reg2data(data, &s) * 1000; /* unit is uV */
unlock:
mutex_unlock(&data->update_lock);
return ret;
}
static int pmbus_regulator_set_voltage(struct regulator_dev *rdev, int min_uv,
@ -3250,16 +3263,22 @@ static int pmbus_regulator_set_voltage(struct regulator_dev *rdev, int min_uv,
};
int val = DIV_ROUND_CLOSEST(min_uv, 1000); /* convert to mV */
int low, high;
int ret;
*selector = 0;
mutex_lock(&data->update_lock);
low = pmbus_regulator_get_low_margin(client, s.page);
if (low < 0)
return low;
if (low < 0) {
ret = low;
goto unlock;
}
high = pmbus_regulator_get_high_margin(client, s.page);
if (high < 0)
return high;
if (high < 0) {
ret = high;
goto unlock;
}
/* Make sure we are within margins */
if (low > val)
@ -3269,7 +3288,10 @@ static int pmbus_regulator_set_voltage(struct regulator_dev *rdev, int min_uv,
val = pmbus_data2reg(data, &s, val);
return _pmbus_write_word_data(client, s.page, PMBUS_VOUT_COMMAND, (u16)val);
ret = _pmbus_write_word_data(client, s.page, PMBUS_VOUT_COMMAND, (u16)val);
unlock:
mutex_unlock(&data->update_lock);
return ret;
}
static int pmbus_regulator_list_voltage(struct regulator_dev *rdev,
@ -3279,6 +3301,7 @@ static int pmbus_regulator_list_voltage(struct regulator_dev *rdev,
struct i2c_client *client = to_i2c_client(dev->parent);
struct pmbus_data *data = i2c_get_clientdata(client);
int val, low, high;
int ret;
if (data->flags & PMBUS_VOUT_PROTECTED)
return 0;
@ -3291,18 +3314,29 @@ static int pmbus_regulator_list_voltage(struct regulator_dev *rdev,
val = DIV_ROUND_CLOSEST(rdev->desc->min_uV +
(rdev->desc->uV_step * selector), 1000); /* convert to mV */
mutex_lock(&data->update_lock);
low = pmbus_regulator_get_low_margin(client, rdev_get_id(rdev));
if (low < 0)
return low;
if (low < 0) {
ret = low;
goto unlock;
}
high = pmbus_regulator_get_high_margin(client, rdev_get_id(rdev));
if (high < 0)
return high;
if (high < 0) {
ret = high;
goto unlock;
}
if (val >= low && val <= high)
return val * 1000; /* unit is uV */
if (val >= low && val <= high) {
ret = val * 1000; /* unit is uV */
goto unlock;
}
return 0;
ret = 0;
unlock:
mutex_unlock(&data->update_lock);
return ret;
}
const struct regulator_ops pmbus_regulator_ops = {
@ -3333,12 +3367,42 @@ int pmbus_regulator_init_cb(struct regulator_dev *rdev,
}
EXPORT_SYMBOL_NS_GPL(pmbus_regulator_init_cb, "PMBUS");
static void pmbus_regulator_notify_work_cancel(void *data)
{
struct pmbus_data *pdata = data;
cancel_work_sync(&pdata->regulator_notify_work);
}
static void pmbus_regulator_notify_worker(struct work_struct *work)
{
struct pmbus_data *data =
container_of(work, struct pmbus_data, regulator_notify_work);
int i, j;
for (i = 0; i < data->info->pages; i++) {
int event;
event = atomic_xchg(&data->regulator_events[i], 0);
if (!event)
continue;
for (j = 0; j < data->info->num_regulators; j++) {
if (i == rdev_get_id(data->rdevs[j])) {
regulator_notifier_call_chain(data->rdevs[j],
event, NULL);
break;
}
}
}
}
static int pmbus_regulator_register(struct pmbus_data *data)
{
struct device *dev = data->dev;
const struct pmbus_driver_info *info = data->info;
const struct pmbus_platform_data *pdata = dev_get_platdata(dev);
int i;
int i, ret;
data->rdevs = devm_kzalloc(dev, sizeof(struct regulator_dev *) * info->num_regulators,
GFP_KERNEL);
@ -3362,19 +3426,19 @@ static int pmbus_regulator_register(struct pmbus_data *data)
info->reg_desc[i].name);
}
INIT_WORK(&data->regulator_notify_work, pmbus_regulator_notify_worker);
ret = devm_add_action_or_reset(dev, pmbus_regulator_notify_work_cancel, data);
if (ret)
return ret;
return 0;
}
static void pmbus_regulator_notify(struct pmbus_data *data, int page, int event)
{
int j;
for (j = 0; j < data->info->num_regulators; j++) {
if (page == rdev_get_id(data->rdevs[j])) {
regulator_notifier_call_chain(data->rdevs[j], event, NULL);
break;
}
}
atomic_or(event, &data->regulator_events[page]);
schedule_work(&data->regulator_notify_work);
}
#else
static int pmbus_regulator_register(struct pmbus_data *data)