From 165cffae1e5c4eb3072dd116aeeb3eea2f26d1f6 Mon Sep 17 00:00:00 2001 From: Matt Borland Date: Wed, 6 May 2026 15:42:33 -0400 Subject: [PATCH 1/3] Fix potential overflow for bases greater than 10 --- .../boost/int128/detail/mini_from_chars.hpp | 67 ++++++++++--------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/include/boost/int128/detail/mini_from_chars.hpp b/include/boost/int128/detail/mini_from_chars.hpp index a399e1d8..92c2f27c 100644 --- a/include/boost/int128/detail/mini_from_chars.hpp +++ b/include/boost/int128/detail/mini_from_chars.hpp @@ -127,7 +127,6 @@ BOOST_INT128_HOST_DEVICE constexpr int from_chars_integer_impl(const char* first overflow_value /= unsigned_base; - overflow_value <<= 1; max_digit %= unsigned_base; // If the only character was a sign abort now @@ -138,48 +137,52 @@ BOOST_INT128_HOST_DEVICE constexpr int from_chars_integer_impl(const char* first bool overflowed = false; - std::ptrdiff_t nc = last - next; - constexpr std::ptrdiff_t nd = std::numeric_limits::digits10; + const std::ptrdiff_t nc = last - next; + // For bases 2..10 the first digits10 characters always fit in the unsigned + // For bases above 10, the safe window is shorter, so we must check with each iteration + const std::ptrdiff_t nd { + base <= 10 + ? static_cast(std::numeric_limits::digits10) + : std::ptrdiff_t{0} + }; + + const std::ptrdiff_t fast_limit {nd < nc ? nd : nc}; + std::ptrdiff_t i = 0; + + for (; i < fast_limit; ++i) { - std::ptrdiff_t i = 0; + const auto current_digit = static_cast(digit_from_char(*next)); - for( ; i < nd && i < nc; ++i ) + if (current_digit >= unsigned_base) { - // overflow is not possible in the first nd characters + break; + } - const auto current_digit = static_cast(digit_from_char(*next)); + result = static_cast(result * unsigned_base + current_digit); + ++next; + } - if (current_digit >= unsigned_base) - { - break; - } + for (; i < nc; ++i) + { + const auto current_digit = static_cast(digit_from_char(*next)); - result = static_cast(result * unsigned_base + current_digit); - ++next; + if (current_digit >= unsigned_base) + { + break; } - for( ; i < nc; ++i ) + if (result < overflow_value || (result == overflow_value && current_digit <= max_digit)) { - const auto current_digit = static_cast(digit_from_char(*next)); - - if (current_digit >= unsigned_base) - { - break; - } - - if (result < overflow_value || (result == overflow_value && current_digit <= max_digit)) - { - result = static_cast(result * unsigned_base + current_digit); - } - else - { - overflowed = true; - break; - } - - ++next; + result = static_cast(result * unsigned_base + current_digit); } + else + { + overflowed = true; + break; + } + + ++next; } // Return the parsed value, adding the sign back if applicable From 96722bf9600a2f048d3bfac1a6cc2a1f2dbb2b6b Mon Sep 17 00:00:00 2001 From: Matt Borland Date: Wed, 6 May 2026 15:43:06 -0400 Subject: [PATCH 2/3] Test edges of overflow --- test/Jamfile | 1 + test/test_from_chars_bases.cpp | 242 +++++++++++++++++++++++++++++++++ 2 files changed, 243 insertions(+) create mode 100644 test/test_from_chars_bases.cpp diff --git a/test/Jamfile b/test/Jamfile index 14b2e609..ac8f4a52 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -65,6 +65,7 @@ run test_climits.cpp ; run test_bit.cpp ; run test_literals.cpp ; +run test_from_chars_bases.cpp ; run test_stream.cpp ; run test_mixed_type_sign_compare.cpp ; diff --git a/test/test_from_chars_bases.cpp b/test/test_from_chars_bases.cpp new file mode 100644 index 00000000..f769c6a8 --- /dev/null +++ b/test/test_from_chars_bases.cpp @@ -0,0 +1,242 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +// Exercises boost::int128::detail::from_chars across every base it supports +// (2..36) for both int128_t and uint128_t. Locks in the fix to the per-iteration +// overflow threshold (the spurious overflow_value <<= 1 was masking MAX+1 +// overflow in mini_from_chars). + +#ifndef BOOST_INT128_BUILD_MODULE + +#include +#include + +#else + +import boost.int128; + +#endif + +#include + +#include +#include +#include +#include + +namespace { + +using boost::int128::int128_t; +using boost::int128::uint128_t; + +// Format a uint128_t into a base-N string (lowercase). Self-contained so the +// test does not pull in boost::charconv just to generate inputs. +std::string format_unsigned(uint128_t value, int base) +{ + if (value == uint128_t{0U}) + { + return "0"; + } + + const auto ubase {static_cast(base)}; + std::string out; + while (value != uint128_t{0U}) + { + const auto digit {static_cast(value % ubase)}; + const char c {digit < 10U ? static_cast('0' + digit) + : static_cast('a' + digit - 10U)}; + out.push_back(c); + value /= ubase; + } + std::reverse(out.begin(), out.end()); + return out; +} + +std::string format_signed(int128_t value, int base) +{ + if (value == int128_t{0}) + { + return "0"; + } + + if (value == (std::numeric_limits::min)()) + { + // |INT128_MIN| does not fit in int128_t; do the magnitude in uint128_t. + const uint128_t magnitude {uint128_t{1} << 127U}; + return std::string{"-"} + format_unsigned(magnitude, base); + } + + if (value < int128_t{0}) + { + return std::string{"-"} + format_unsigned(static_cast(-value), base); + } + + return format_unsigned(static_cast(value), base); +} + +template +void check_roundtrip(T expected, int base) +{ + std::string s; + if constexpr (std::is_same_v) + { + s = format_signed(expected, base); + } + else + { + s = format_unsigned(expected, base); + } + + T parsed {}; + const auto r {boost::int128::detail::from_chars(s.data(), s.data() + s.size(), parsed, base)}; + + BOOST_TEST_LT(r, 0); + BOOST_TEST(parsed == expected); +} + +template +void check_overflow(const std::string& s, int base) +{ + T parsed {}; + const auto r {boost::int128::detail::from_chars(s.data(), s.data() + s.size(), parsed, base)}; + + BOOST_TEST_EQ(r, EDOM); +} + +void test_uint128_all_bases() +{ + constexpr auto max_value {(std::numeric_limits::max)()}; + + for (int base {2}; base <= 36; ++base) + { + // Canonical small values. + check_roundtrip(uint128_t{0U}, base); + check_roundtrip(uint128_t{1U}, base); + check_roundtrip(static_cast(static_cast(base) - 1U), base); + check_roundtrip(static_cast(static_cast(base)), base); + + // A handful of mid-range values that span the per-base digit window. + check_roundtrip(uint128_t{42U}, base); + check_roundtrip(uint128_t{1234567890U}, base); + check_roundtrip(uint128_t{0xFFFFFFFFFFFFFFFFULL}, base); + check_roundtrip(uint128_t{1U} << 100U, base); + + // The boundary itself parses correctly. + check_roundtrip(max_value, base); + + // MAX with any extra digit appended is at least MAX * base, which + // overflows uint128_t for every base in [2, 36]. + const auto max_str {format_unsigned(max_value, base)}; + check_overflow(max_str + "0", base); + } +} + +void test_int128_all_bases() +{ + constexpr auto max_value {(std::numeric_limits::max)()}; + constexpr auto min_value {(std::numeric_limits::min)()}; + + for (int base {2}; base <= 36; ++base) + { + check_roundtrip(int128_t{0}, base); + check_roundtrip(int128_t{1}, base); + check_roundtrip(int128_t{-1}, base); + check_roundtrip(int128_t{42}, base); + check_roundtrip(int128_t{-42}, base); + check_roundtrip(int128_t{1234567890}, base); + check_roundtrip(int128_t{-1234567890}, base); + + // Both signed boundaries parse correctly. + check_roundtrip(max_value, base); + check_roundtrip(min_value, base); + + // Append a digit to push past the magnitude bound on each side. + const auto max_str {format_signed(max_value, base)}; + check_overflow(max_str + "0", base); + + const auto min_str {format_signed(min_value, base)}; + check_overflow(min_str + "0", base); + } +} + +void test_decimal_boundaries() +{ + // Tight base-10 boundary cases: the spurious <<= 1 in the threshold made + // these silently produce wrong values instead of returning EDOM. + + // UINT128_MAX exactly. + { + const std::string s {"340282366920938463463374607431768211455"}; + uint128_t v {}; + const auto r {boost::int128::detail::from_chars(s.data(), s.data() + s.size(), v)}; + BOOST_TEST_LT(r, 0); + BOOST_TEST(v == (std::numeric_limits::max)()); + } + + // UINT128_MAX + 1. + check_overflow("340282366920938463463374607431768211456", 10); + + // INT128_MAX exactly. + { + const std::string s {"170141183460469231731687303715884105727"}; + int128_t v {}; + const auto r {boost::int128::detail::from_chars(s.data(), s.data() + s.size(), v)}; + BOOST_TEST_LT(r, 0); + BOOST_TEST(v == (std::numeric_limits::max)()); + } + + // INT128_MAX + 1. + check_overflow("170141183460469231731687303715884105728", 10); + + // INT128_MIN exactly. + { + const std::string s {"-170141183460469231731687303715884105728"}; + int128_t v {}; + const auto r {boost::int128::detail::from_chars(s.data(), s.data() + s.size(), v)}; + BOOST_TEST_LT(r, 0); + BOOST_TEST(v == (std::numeric_limits::min)()); + } + + // INT128_MIN - 1. + check_overflow("-170141183460469231731687303715884105729", 10); +} + +void test_invalid_inputs() +{ + // Empty range is EINVAL. + { + const char* s {""}; + uint128_t v {}; + const auto r {boost::int128::detail::from_chars(s, s, v)}; + BOOST_TEST_EQ(r, EINVAL); + } + + // Lone sign is EINVAL. + { + const std::string s {"-"}; + int128_t v {}; + const auto r {boost::int128::detail::from_chars(s.data(), s.data() + s.size(), v)}; + BOOST_TEST_EQ(r, EINVAL); + } + + // Leading sign on the unsigned overload is EINVAL. + { + const std::string s {"-1"}; + uint128_t v {}; + const auto r {boost::int128::detail::from_chars(s.data(), s.data() + s.size(), v)}; + BOOST_TEST_EQ(r, EINVAL); + } +} + +} // anonymous namespace + +int main() +{ + test_uint128_all_bases(); + test_int128_all_bases(); + test_decimal_boundaries(); + test_invalid_inputs(); + + return boost::report_errors(); +} From 7054398341fd186c57c1a14f7f9c25d391b6339a Mon Sep 17 00:00:00 2001 From: Matt Borland Date: Wed, 6 May 2026 21:18:12 -0400 Subject: [PATCH 3/3] Fix usage of C++17 features --- test/test_from_chars_bases.cpp | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/test/test_from_chars_bases.cpp b/test/test_from_chars_bases.cpp index f769c6a8..85794d29 100644 --- a/test/test_from_chars_bases.cpp +++ b/test/test_from_chars_bases.cpp @@ -75,18 +75,20 @@ std::string format_signed(int128_t value, int base) return format_unsigned(static_cast(value), base); } +inline std::string format_value(int128_t value, int base) +{ + return format_signed(value, base); +} + +inline std::string format_value(uint128_t value, int base) +{ + return format_unsigned(value, base); +} + template void check_roundtrip(T expected, int base) { - std::string s; - if constexpr (std::is_same_v) - { - s = format_signed(expected, base); - } - else - { - s = format_unsigned(expected, base); - } + const std::string s {format_value(expected, base)}; T parsed {}; const auto r {boost::int128::detail::from_chars(s.data(), s.data() + s.size(), parsed, base)};