Skip to content

Commit d694116

Browse files
committed
fix signed integer overflow in modbus_get_float_*()
The byte assembly `i = (a << 24) | (b << 16) | (c << 8) | d` in the four modbus_get_float_*() variants operates on uint8_t values, which promote to signed int. When the most significant byte a is >= 0x80, `a << 24` overflows a 32-bit signed int, which is undefined behavior. Reported by Dominik Blain — Qreativelab Formal Verification.
1 parent 5852b90 commit d694116

2 files changed

Lines changed: 27 additions & 4 deletions

File tree

src/modbus-data.c

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,11 @@ float modbus_get_float_abcd(const uint16_t *src)
9292
d = (src[1] >> 0) & 0xFF; // low byte of second word
9393

9494
// we assemble 32bit integer always in abcd order via shift operations
95-
i = (a << 24) | (b << 16) | (c << 8) | (d << 0);
95+
// Cast to uint32_t before shifting: a..d promote to signed int, and
96+
// "a << 24" with a >= 0x80 would overflow a 32-bit signed int (undefined
97+
// behavior). Assembling in unsigned width keeps it well-defined. The same
98+
// applies to the identical assembly in the dcba/badc/cdab variants below.
99+
i = ((uint32_t) a << 24) | ((uint32_t) b << 16) | ((uint32_t) c << 8) | d;
96100
memcpy(&f, &i, 4);
97101

98102
return f;
@@ -112,7 +116,7 @@ float modbus_get_float_dcba(const uint16_t *src)
112116
a = (src[1] >> 0) & 0xFF;
113117

114118
// we assemble 32bit integer always in abcd order via shift operations
115-
i = (a << 24) | (b << 16) | (c << 8) | (d << 0);
119+
i = ((uint32_t) a << 24) | ((uint32_t) b << 16) | ((uint32_t) c << 8) | d;
116120
memcpy(&f, &i, 4);
117121

118122
return f;
@@ -132,7 +136,7 @@ float modbus_get_float_badc(const uint16_t *src)
132136
c = (src[1] >> 0) & 0xFF;
133137

134138
// we assemble 32bit integer always in abcd order via shift operations
135-
i = (a << 24) | (b << 16) | (c << 8) | (d << 0);
139+
i = ((uint32_t) a << 24) | ((uint32_t) b << 16) | ((uint32_t) c << 8) | d;
136140
memcpy(&f, &i, 4);
137141

138142
return f;
@@ -152,7 +156,7 @@ float modbus_get_float_cdab(const uint16_t *src)
152156
b = (src[1] >> 0) & 0xFF;
153157

154158
// we assemble 32bit integer always in abcd order via shift operations
155-
i = (a << 24) | (b << 16) | (c << 8) | (d << 0);
159+
i = ((uint32_t) a << 24) | ((uint32_t) b << 16) | ((uint32_t) c << 8) | d;
156160
memcpy(&f, &i, 4);
157161

158162
return f;

tests/unit-test-client.c

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,25 @@ int main(int argc, char *argv[])
349349
real = modbus_get_float_cdab(UT_IREAL_CDAB);
350350
ASSERT_TRUE(real == UT_REAL, "FAILED (%f != %f)\n", real, UT_REAL);
351351

352+
/* Regression: a negative float has its most significant byte >= 0x80, so
353+
the "a << 24" byte assembly in modbus_get_float_*() overflows a signed
354+
int (undefined behavior) unless done in unsigned width. UT_REAL is
355+
positive (0x47F12000) and never exercised this path; -UT_REAL
356+
(0xC7F12000) does. Build with -fsanitize=undefined to catch the UB. */
357+
printf("Get negative float ABCD/DCBA/BADC/CDAB: ");
358+
{
359+
const uint16_t ireal_neg_abcd[] = {0xC7F1, 0x2000};
360+
const uint16_t ireal_neg_dcba[] = {0x0020, 0xF1C7};
361+
const uint16_t ireal_neg_badc[] = {0xF1C7, 0x0020};
362+
const uint16_t ireal_neg_cdab[] = {0x2000, 0xC7F1};
363+
364+
ASSERT_TRUE(modbus_get_float_abcd(ireal_neg_abcd) == -UT_REAL &&
365+
modbus_get_float_dcba(ireal_neg_dcba) == -UT_REAL &&
366+
modbus_get_float_badc(ireal_neg_badc) == -UT_REAL &&
367+
modbus_get_float_cdab(ireal_neg_cdab) == -UT_REAL,
368+
"FAILED negative float get\n");
369+
}
370+
352371
printf("\nAt this point, error messages doesn't mean the test has failed\n");
353372

354373
/** ILLEGAL DATA ADDRESS **/

0 commit comments

Comments
 (0)