mirror of
https://github.com/torvalds/linux.git
synced 2026-07-28 01:55:51 +02:00
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:
parent
cd658475e7
commit
754bd2b4a0
|
|
@ -6,6 +6,7 @@
|
||||||
* Copyright (c) 2012 Guenter Roeck
|
* Copyright (c) 2012 Guenter Roeck
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#include <linux/atomic.h>
|
||||||
#include <linux/debugfs.h>
|
#include <linux/debugfs.h>
|
||||||
#include <linux/delay.h>
|
#include <linux/delay.h>
|
||||||
#include <linux/dcache.h>
|
#include <linux/dcache.h>
|
||||||
|
|
@ -21,8 +22,8 @@
|
||||||
#include <linux/pmbus.h>
|
#include <linux/pmbus.h>
|
||||||
#include <linux/regulator/driver.h>
|
#include <linux/regulator/driver.h>
|
||||||
#include <linux/regulator/machine.h>
|
#include <linux/regulator/machine.h>
|
||||||
#include <linux/of.h>
|
|
||||||
#include <linux/thermal.h>
|
#include <linux/thermal.h>
|
||||||
|
#include <linux/workqueue.h>
|
||||||
#include "pmbus.h"
|
#include "pmbus.h"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
@ -112,6 +113,11 @@ struct pmbus_data {
|
||||||
|
|
||||||
struct mutex update_lock;
|
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 */
|
bool has_status_word; /* device uses STATUS_WORD register */
|
||||||
int (*read_status)(struct i2c_client *client, int page);
|
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,
|
.class = PSC_VOLTAGE_OUT,
|
||||||
.convert = true,
|
.convert = true,
|
||||||
};
|
};
|
||||||
|
int ret;
|
||||||
|
|
||||||
|
mutex_lock(&data->update_lock);
|
||||||
s.data = _pmbus_read_word_data(client, s.page, 0xff, PMBUS_READ_VOUT);
|
s.data = _pmbus_read_word_data(client, s.page, 0xff, PMBUS_READ_VOUT);
|
||||||
if (s.data < 0)
|
if (s.data < 0) {
|
||||||
return s.data;
|
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,
|
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 val = DIV_ROUND_CLOSEST(min_uv, 1000); /* convert to mV */
|
||||||
int low, high;
|
int low, high;
|
||||||
|
int ret;
|
||||||
|
|
||||||
*selector = 0;
|
*selector = 0;
|
||||||
|
|
||||||
|
mutex_lock(&data->update_lock);
|
||||||
low = pmbus_regulator_get_low_margin(client, s.page);
|
low = pmbus_regulator_get_low_margin(client, s.page);
|
||||||
if (low < 0)
|
if (low < 0) {
|
||||||
return low;
|
ret = low;
|
||||||
|
goto unlock;
|
||||||
|
}
|
||||||
|
|
||||||
high = pmbus_regulator_get_high_margin(client, s.page);
|
high = pmbus_regulator_get_high_margin(client, s.page);
|
||||||
if (high < 0)
|
if (high < 0) {
|
||||||
return high;
|
ret = high;
|
||||||
|
goto unlock;
|
||||||
|
}
|
||||||
|
|
||||||
/* Make sure we are within margins */
|
/* Make sure we are within margins */
|
||||||
if (low > val)
|
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);
|
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,
|
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 i2c_client *client = to_i2c_client(dev->parent);
|
||||||
struct pmbus_data *data = i2c_get_clientdata(client);
|
struct pmbus_data *data = i2c_get_clientdata(client);
|
||||||
int val, low, high;
|
int val, low, high;
|
||||||
|
int ret;
|
||||||
|
|
||||||
if (data->flags & PMBUS_VOUT_PROTECTED)
|
if (data->flags & PMBUS_VOUT_PROTECTED)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -3291,18 +3314,29 @@ static int pmbus_regulator_list_voltage(struct regulator_dev *rdev,
|
||||||
val = DIV_ROUND_CLOSEST(rdev->desc->min_uV +
|
val = DIV_ROUND_CLOSEST(rdev->desc->min_uV +
|
||||||
(rdev->desc->uV_step * selector), 1000); /* convert to mV */
|
(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));
|
low = pmbus_regulator_get_low_margin(client, rdev_get_id(rdev));
|
||||||
if (low < 0)
|
if (low < 0) {
|
||||||
return low;
|
ret = low;
|
||||||
|
goto unlock;
|
||||||
|
}
|
||||||
|
|
||||||
high = pmbus_regulator_get_high_margin(client, rdev_get_id(rdev));
|
high = pmbus_regulator_get_high_margin(client, rdev_get_id(rdev));
|
||||||
if (high < 0)
|
if (high < 0) {
|
||||||
return high;
|
ret = high;
|
||||||
|
goto unlock;
|
||||||
|
}
|
||||||
|
|
||||||
if (val >= low && val <= high)
|
if (val >= low && val <= high) {
|
||||||
return val * 1000; /* unit is uV */
|
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 = {
|
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");
|
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)
|
static int pmbus_regulator_register(struct pmbus_data *data)
|
||||||
{
|
{
|
||||||
struct device *dev = data->dev;
|
struct device *dev = data->dev;
|
||||||
const struct pmbus_driver_info *info = data->info;
|
const struct pmbus_driver_info *info = data->info;
|
||||||
const struct pmbus_platform_data *pdata = dev_get_platdata(dev);
|
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,
|
data->rdevs = devm_kzalloc(dev, sizeof(struct regulator_dev *) * info->num_regulators,
|
||||||
GFP_KERNEL);
|
GFP_KERNEL);
|
||||||
|
|
@ -3362,19 +3426,19 @@ static int pmbus_regulator_register(struct pmbus_data *data)
|
||||||
info->reg_desc[i].name);
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void pmbus_regulator_notify(struct pmbus_data *data, int page, int event)
|
static void pmbus_regulator_notify(struct pmbus_data *data, int page, int event)
|
||||||
{
|
{
|
||||||
int j;
|
atomic_or(event, &data->regulator_events[page]);
|
||||||
|
schedule_work(&data->regulator_notify_work);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
static int pmbus_regulator_register(struct pmbus_data *data)
|
static int pmbus_regulator_register(struct pmbus_data *data)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user