[PATCH 1/2] hwmon: (ltc2992) Clamp threshold writes to hardware range

From: Pradhan, Sanman

Date: Thu Apr 16 2026 - 18:01:08 EST


From: Sanman Pradhan <psanman@xxxxxxxxxxx>

ltc2992_set_voltage(), ltc2992_set_current(), and ltc2992_set_power()
do not validate the user-supplied value before converting it to a
register value. This can result in:

1. Negative input values wrapping to large positive register values.
For power, the negative long is implicitly cast to u64 in
mul_u64_u32_div(), producing an incorrect value. For voltage and
current, the negative converted value wraps when passed to
ltc2992_write_reg() as a u32.

2. Intermediate arithmetic exceeding the range representable in u64 on
64-bit platforms. In ltc2992_set_voltage(), (u64)val * 1000 can
exceed U64_MAX when val is a large positive long. In
ltc2992_set_current(), (u64)val * r_sense_uohm can overflow
similarly. In ltc2992_set_power(), the computed value may not fit
in u64.

3. Register values exceeding the hardware field width. Voltage and
current threshold registers are 12-bit (stored left-justified in
16 bits), and power threshold registers are 24-bit. Without
clamping, bits above the field width are truncated in
ltc2992_write_reg().

Fix by clamping negative values to zero, clamping positive values to
the rounded hardware-representable maximum (the value returned by the
read path for a full-scale register) to prevent intermediate overflow,
and clamping the converted register value to the hardware field width
before writing. The existing conversion formula and rounding behavior
are preserved.

In the power write path, cancel the factor of 1000 from both the
numerator (r_sense_uohm * 1000) and the denominator
(VADC_UV_LSB * IADC_NANOV_LSB) to also eliminate a u32 overflow of
r_sense_uohm * 1000 when r_sense_uohm exceeds about 4.29 ohms.

Fixes: b0bd407e94b03 ("hwmon: (ltc2992) Add support")
Cc: stable@xxxxxxxxxxxxxxx
Signed-off-by: Sanman Pradhan <psanman@xxxxxxxxxxx>
---
drivers/hwmon/ltc2992.c | 37 +++++++++++++++++++++++++++++--------
1 file changed, 29 insertions(+), 8 deletions(-)

diff --git a/drivers/hwmon/ltc2992.c b/drivers/hwmon/ltc2992.c
index 1fcd320d61619..1069736196763 100644
--- a/drivers/hwmon/ltc2992.c
+++ b/drivers/hwmon/ltc2992.c
@@ -431,10 +431,16 @@ static int ltc2992_get_voltage(struct ltc2992_state *st, u32 reg, u32 scale, lon

static int ltc2992_set_voltage(struct ltc2992_state *st, u32 reg, u32 scale, long val)
{
- val = DIV_ROUND_CLOSEST(val * 1000, scale);
- val = val << 4;
+ u32 reg_val;
+ long vmax;
+
+ vmax = DIV_ROUND_CLOSEST_ULL(0xFFFULL * scale, 1000);
+ val = max(val, 0L);
+ val = min(val, vmax);
+ reg_val = min(DIV_ROUND_CLOSEST_ULL((u64)val * 1000, scale),
+ 0xFFFULL) << 4;

- return ltc2992_write_reg(st, reg, 2, val);
+ return ltc2992_write_reg(st, reg, 2, reg_val);
}

static int ltc2992_read_gpio_alarm(struct ltc2992_state *st, int nr_gpio, u32 attr, long *val)
@@ -559,9 +565,15 @@ static int ltc2992_get_current(struct ltc2992_state *st, u32 reg, u32 channel, l
static int ltc2992_set_current(struct ltc2992_state *st, u32 reg, u32 channel, long val)
{
u32 reg_val;
+ long cmax;

- reg_val = DIV_ROUND_CLOSEST(val * st->r_sense_uohm[channel], LTC2992_IADC_NANOV_LSB);
- reg_val = reg_val << 4;
+ cmax = DIV_ROUND_CLOSEST_ULL(0xFFFULL * LTC2992_IADC_NANOV_LSB,
+ st->r_sense_uohm[channel]);
+ val = max(val, 0L);
+ val = min(val, cmax);
+ reg_val = min(DIV_ROUND_CLOSEST_ULL((u64)val * st->r_sense_uohm[channel],
+ LTC2992_IADC_NANOV_LSB),
+ 0xFFFULL) << 4;

return ltc2992_write_reg(st, reg, 2, reg_val);
}
@@ -634,9 +646,18 @@ static int ltc2992_get_power(struct ltc2992_state *st, u32 reg, u32 channel, lon
static int ltc2992_set_power(struct ltc2992_state *st, u32 reg, u32 channel, long val)
{
u32 reg_val;
-
- reg_val = mul_u64_u32_div(val, st->r_sense_uohm[channel] * 1000,
- LTC2992_VADC_UV_LSB * LTC2992_IADC_NANOV_LSB);
+ u64 pmax, uval;
+
+ uval = max(val, 0L);
+ pmax = mul_u64_u32_div(0xFFFFFFULL,
+ LTC2992_VADC_UV_LSB / 1000 *
+ LTC2992_IADC_NANOV_LSB,
+ st->r_sense_uohm[channel]);
+ uval = min(uval, pmax);
+ reg_val = min(mul_u64_u32_div(uval, st->r_sense_uohm[channel],
+ LTC2992_VADC_UV_LSB / 1000 *
+ LTC2992_IADC_NANOV_LSB),
+ 0xFFFFFFULL);

return ltc2992_write_reg(st, reg, 3, reg_val);
}
--
2.34.1