utils/big_decimal: fix scale overflow when parsing values with large exponents

The exponent of a big decimal string is parsed as an int32, adjusted for
the removed fractional part, and stored as an int32. When parsing values
like `1.23E-2147483647`, the unscaled value becomes `123`, and the scale
is adjusted to `2147483647 + 2 = 2147483649`. This exceeds the int32
limit, and since the scale is stored as an int32, it overflows and wraps
around, losing the value.

This patch fixes that the by parsing the exponent as an int64 value and
then adjusting it for the fractional part. The adjusted scale is then
checked to see if it is still within int32 limits before storing. An
exception is thrown if it is not within the int32 limits.

Note that strings with exponents that exceed the int32 range, like
`0.01E2147483650`, were previously not parseable as a big decimal. They
are now accepted if the final adjusted scale fits within int32 limits.
For the above value, unscaled_value = 1 and scale = -2147483648, so it
is now accepted. This is in line with how Java's `BigDecimal` parses
strings.

Fixes: #24581

Signed-off-by: Lakshmi Narayanan Sreethar <lakshmi.sreethar@scylladb.com>

Closes scylladb/scylladb#24640

(cherry picked from commit 279253ffd0)

Closes scylladb/scylladb#24691
This commit is contained in:
Lakshmi Narayanan Sreethar
2025-06-25 13:26:44 +05:30
committed by Botond Dénes
parent 1d78f17697
commit 99ec69e27d
2 changed files with 25 additions and 2 deletions

View File

@@ -99,6 +99,23 @@ BOOST_AUTO_TEST_CASE(test_big_decimal_construct_from_string) {
BOOST_REQUIRE_THROW(big_decimal("+-5"), marshal_exception);
BOOST_REQUIRE_THROW(big_decimal("++5"), marshal_exception);
BOOST_REQUIRE_THROW(big_decimal("--5"), marshal_exception);
// Verify large exponent gets parsed correctly
// 1E-2147483647 : scale = 2147483647; OK
BOOST_REQUIRE_NO_THROW(big_decimal("1E-2147483647"));
// 1E2147483648 : scale = -2147483648; OK
BOOST_REQUIRE_NO_THROW(big_decimal("1E2147483648"));
// 0.01E2147483650 : scale = -2147483648;
// exponent is > int32::max() but the adjusted scale is still within int32 limits, so it is OK.
BOOST_REQUIRE_NO_THROW(big_decimal("0.01E2147483650"));
// Any overflow to scale should throw marshal_exception.
// 1E-2147483648 : scale(2147483648) > int32::max()
BOOST_REQUIRE_THROW(big_decimal("1E-2147483648"), marshal_exception);
// 1E2147483649 : scale(-2147483649) < int32::min()
BOOST_REQUIRE_THROW(big_decimal("1E2147483649"), marshal_exception);
// 1.2E-2147483647 : scale(2147483648) > int32::max()
BOOST_REQUIRE_THROW(big_decimal("1.2E-2147483647"), marshal_exception);
}
BOOST_AUTO_TEST_CASE(test_big_decimal_div) {

View File

@@ -78,12 +78,18 @@ big_decimal::big_decimal(std::string_view text)
if (negative) {
_unscaled_value *= -1;
}
// parse scale as int64_t, so that it can be adjusted with fraction size and then checked for overflow.
int64_t scale = 0;
try {
_scale = exponent.empty() ? 0 : -boost::lexical_cast<int32_t>(exponent);
scale = exponent.empty() ? 0 : -boost::lexical_cast<int64_t>(exponent);
} catch (...) {
throw marshal_exception(seastar::format("big_decimal - failed to parse exponent: {}", exponent));
}
_scale += fraction.size();
scale += fraction.size();
if (scale < std::numeric_limits<int32_t>::min() || scale > std::numeric_limits<int32_t>::max()) {
throw marshal_exception(seastar::format("big_decimal - scale out of range: {}", scale));
}
_scale = static_cast<int32_t>(scale);
}
boost::multiprecision::cpp_rational big_decimal::as_rational() const {