]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
hwmon: (ltc2992) Clamp threshold writes to hardware range
authorSanman Pradhan <psanman@juniper.net>
Thu, 16 Apr 2026 21:59:30 +0000 (21:59 +0000)
committerGuenter Roeck <linux@roeck-us.net>
Thu, 30 Apr 2026 17:22:48 +0000 (10:22 -0700)
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@vger.kernel.org
Signed-off-by: Sanman Pradhan <psanman@juniper.net>
Link: https://lore.kernel.org/r/20260416215904.101969-2-sanman.pradhan@hpe.com
Signed-off-by: Guenter Roeck <linux@roeck-us.net>
drivers/hwmon/ltc2992.c

index 1fcd320d6161970fbd6e422a79dadfe8bb78a390..1069736196763de52929e882701e6dcb82b860c5 100644 (file)
@@ -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);
 }