From 99ec69e27d100d197cc1a598fa20df4cbace3db5 Mon Sep 17 00:00:00 2001 From: Lakshmi Narayanan Sreethar Date: Wed, 25 Jun 2025 13:26:44 +0530 Subject: [PATCH] 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 Closes scylladb/scylladb#24640 (cherry picked from commit 279253ffd0f9b769a6b38ceab4d99a87a66bef57) Closes scylladb/scylladb#24691 --- test/boost/big_decimal_test.cc | 17 +++++++++++++++++ utils/big_decimal.cc | 10 ++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/test/boost/big_decimal_test.cc b/test/boost/big_decimal_test.cc index 1f744a1edd..ac94ee3964 100644 --- a/test/boost/big_decimal_test.cc +++ b/test/boost/big_decimal_test.cc @@ -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) { diff --git a/utils/big_decimal.cc b/utils/big_decimal.cc index b83c86d18d..698087a213 100644 --- a/utils/big_decimal.cc +++ b/utils/big_decimal.cc @@ -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(exponent); + scale = exponent.empty() ? 0 : -boost::lexical_cast(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::min() || scale > std::numeric_limits::max()) { + throw marshal_exception(seastar::format("big_decimal - scale out of range: {}", scale)); + } + _scale = static_cast(scale); } boost::multiprecision::cpp_rational big_decimal::as_rational() const {