From 7b5c5043186bb59d2e53959493823dc4af6e6ced Mon Sep 17 00:00:00 2001 From: sahvx655-wq Date: Sat, 13 Jun 2026 21:20:45 +0530 Subject: [PATCH] detect uint64 overflow that wraps past min_safe in parse_int_string --- include/fast_float/ascii_number.h | 25 ++++++++++++-- tests/fast_int.cpp | 55 +++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 3 deletions(-) diff --git a/include/fast_float/ascii_number.h b/include/fast_float/ascii_number.h index b54c10b3..f895e9ba 100644 --- a/include/fast_float/ascii_number.h +++ b/include/fast_float/ascii_number.h @@ -781,9 +781,28 @@ parse_int_string(UC const *p, UC const *pend, T &value, } // this check can be eliminated for all other types, but they will all require // a max_digits(base) equivalent - if (digit_count == max_digits && i < min_safe_u64(base)) { - answer.ec = std::errc::result_out_of_range; - return answer; + if (digit_count == max_digits) { + // A value that wrapped below the smallest max_digits-length value has + // certainly overflowed. + if (i < min_safe_u64(base)) { + answer.ec = std::errc::result_out_of_range; + return answer; + } + // i >= min_safe_u64(base) is still not proof that it fits: for any base + // whose max_digits-length range exceeds 2^64 (base 10 reaches ~5.4 * 2^64 + // at 20 digits) the accumulator can wrap a whole multiple of 2^64 and land + // back above min_safe, so the test above lets that overflow through. Re-run + // the parsed digits with a checked multiply-add to decide exactly. + uint64_t overflow_check = 0; + for (UC const *q = start_digits; q != p; ++q) { + uint8_t const digit = ch_to_digit(*q); + if (overflow_check > + (std::numeric_limits::max() - digit) / uint64_t(base)) { + answer.ec = std::errc::result_out_of_range; + return answer; + } + overflow_check = uint64_t(base) * overflow_check + digit; + } } // check other types overflow diff --git a/tests/fast_int.cpp b/tests/fast_int.cpp index 0a78faae..9762f967 100644 --- a/tests/fast_int.cpp +++ b/tests/fast_int.cpp @@ -821,6 +821,61 @@ int main() { ++base_unsigned; } + // unsigned out of range error base test, multi-wrap (64 bit) + // These values overflow uint64_t, but the accumulator wraps a whole multiple + // of 2^64 and lands back at or above the smallest max_digits-length value, so + // a single comparison against that bound does not catch the overflow. Bases + // 2, 4 and 16 are excluded because their max_digits-length range fits within + // a single 2^64 span. + std::vector const unsigned_multiwrap_base{ + 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17, 18, 19, 20, + 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36}; + std::vector const unsigned_multiwrap_base_test{ + "22222222222222222222222222222222222222222", + "4400000000000000000000000000", + "5555555555555555555555555", + "66666666666666666666666", + "7777777777777777777777", + "888888888888888888888", + "46893488147419103233", + "AAAAAAAAAAAAAAAAAAA", + "BBBBBBBBBBBBBBBBBB", + "427772311192C9BAAB", + "DDDDDDDDDDDDDDDDD", + "532C82996D3A44919", + "GGGGGGGGGGGGGGGG", + "HHHHHHHHHHHHHHHH", + "3835GEGDF36622EG", + "JJJJJJJJJJJJJJJ", + "KKKKKKKKKKKKKKK", + "LLLLLLLLLLLLLLL", + "444BGHB4EG5DA2D", + "NNNNNNNNNNNNNN", + "JE5H4MNDLJGNLO", + "PPPPPPPPPPPPPP", + "QQQQQQQQQQQQQQ", + "RRRRRRRRRRRRRR", + "4H7QS52310IHQK", + "TTTTTTTTTTTTTT", + "UUUUUUUUUUUUU", + "VVVVVVVVVVVVV", + "WWWWWWWWWWWWW", + "XXXXXXXXXXXXX", + "YYYYYYYYYYYYY", + "6U831JL976P6O"}; + + for (std::size_t i = 0; i < unsigned_multiwrap_base_test.size(); ++i) { + auto const &f = unsigned_multiwrap_base_test[i]; + uint64_t result; + auto answer = fast_float::from_chars(f.data(), f.data() + f.size(), result, + unsigned_multiwrap_base[i]); + if (answer.ec != std::errc::result_out_of_range) { + std::cerr << "expected error for should be 'result_out_of_range': \"" << f + << "\"" << std::endl; + return EXIT_FAILURE; + } + } + // just within range base test (64 bit) std::vector const int_within_range_base_test{ "111111111111111111111111111111111111111111111111111111111111111",