From 4463a281447e6c39fbac7771be40fdff263d0ba2 Mon Sep 17 00:00:00 2001 From: Aysha Afrah Ziya Date: Sat, 24 Oct 2026 13:20:04 +0530 Subject: [PATCH 1/2] reject out-of-range values in the double to float conversions convertNumber skipped every check for a floating-to-floating conversion, so Any::cast of a double beyond the float range was a bare static_cast that yields an infinity (undefined per [conv.double]). The convertFromString and Any::stringToNumber fallbacks used without the floating-point std::from_chars had the same cast, while the from_chars branch rejects such literals as out of range. --- .../behaviortree_cpp/utils/convert_impl.hpp | 22 +++++++++----- include/behaviortree_cpp/utils/safe_any.hpp | 5 +++- src/basic_types.cpp | 9 ++++++ tests/gtest_basic_types.cpp | 29 +++++++++++++++++++ 4 files changed, 57 insertions(+), 8 deletions(-) diff --git a/include/behaviortree_cpp/utils/convert_impl.hpp b/include/behaviortree_cpp/utils/convert_impl.hpp index e32eefbcf..29115c9c7 100644 --- a/include/behaviortree_cpp/utils/convert_impl.hpp +++ b/include/behaviortree_cpp/utils/convert_impl.hpp @@ -128,6 +128,20 @@ inline void checkTruncation(const From& from) throw std::runtime_error("Invalid floating point to integer conversion"); } } + // Handle floating point to floating point (e.g. double to float) + else if constexpr(std::is_floating_point_v && std::is_floating_point_v) + { + // Rounding to the narrower type is accepted, but a finite value beyond the + // range of To has no representation there: the conversion is undefined and + // in practice produces an infinity. + if(std::isfinite(from) && + (from > static_cast(std::numeric_limits::max()) || + from < static_cast(std::numeric_limits::lowest()))) + { + throw std::runtime_error("Value outside the numeric limits of the floating point " + "type"); + } + } // Handle other conversions else { @@ -198,13 +212,7 @@ void convertNumber(const SRC& source, DST& target) else if constexpr(std::is_floating_point::value || std::is_floating_point::value) { - bool both_float = - std::is_floating_point::value && std::is_floating_point::value; - // to avoid being too pedantic, let's accept casting between double and float - if(!both_float) - { - checkTruncation(source); - } + checkTruncation(source); target = static_cast(source); } } diff --git a/include/behaviortree_cpp/utils/safe_any.hpp b/include/behaviortree_cpp/utils/safe_any.hpp index 7a9a9cb53..b7f4b4135 100644 --- a/include/behaviortree_cpp/utils/safe_any.hpp +++ b/include/behaviortree_cpp/utils/safe_any.hpp @@ -471,7 +471,10 @@ inline nonstd::expected Any::stringToNumber() const double value = 0.0; if(parseDouble(str.toStdStringView(), value, /*require_full_consumption=*/false)) { - return static_cast(value); + // same range check as the integral branch: a value that does not fit + // in a float is an error, like std::from_chars reports above + Any temp_any(value); + return temp_any.convert(); } return nonstd::make_unexpected("Any failed string to number conversion"); } diff --git a/src/basic_types.cpp b/src/basic_types.cpp index e4c1554eb..172f3ebac 100644 --- a/src/basic_types.cpp +++ b/src/basic_types.cpp @@ -11,6 +11,8 @@ #if !defined(__cpp_lib_to_chars) || (__cpp_lib_to_chars < 201611L) #include #include +#include +#include #include #if defined(__APPLE__) @@ -299,6 +301,13 @@ float convertFromString(StringView str) { throw RuntimeError(StrCat("Can't convert string [", str, "] to float")); } + // std::from_chars reports result_out_of_range for a finite value that + // does not fit in a float; casting the double would silently give an infinity. + if(std::isfinite(result) && + std::fabs(result) > static_cast(std::numeric_limits::max())) + { + throw RuntimeError(StrCat("Can't convert string [", str, "] to float")); + } return static_cast(result); #endif } diff --git a/tests/gtest_basic_types.cpp b/tests/gtest_basic_types.cpp index 680c45ce3..0a1bccfca 100644 --- a/tests/gtest_basic_types.cpp +++ b/tests/gtest_basic_types.cpp @@ -148,6 +148,35 @@ TEST(BasicTypes, DoubleToIntBoundary) EXPECT_NO_THROW(bb->set("k", in_range)); } +TEST(BasicTypes, FloatConversionLimits) +{ + // A double beyond the float range has no float representation: the cast is + // undefined and used to return an infinity. Reject it like every other + // out-of-range narrowing. + EXPECT_ANY_THROW((void)BT::Any(1e300).cast()); + EXPECT_ANY_THROW((void)BT::Any(-1e300).cast()); + + // Rounding to float is still accepted, as are values close to the limit. + EXPECT_FLOAT_EQ(BT::Any(0.1).cast(), 0.1f); + EXPECT_FLOAT_EQ(BT::Any(3.4e38).cast(), 3.4e38f); + EXPECT_FLOAT_EQ(BT::Any(-3.4e38).cast(), -3.4e38f); + + // Same path through a blackboard entry holding a double. + auto bb = BT::Blackboard::create(); + bb->set("v", 1e300); + EXPECT_ANY_THROW((void)bb->get("v")); + bb->set("v", 2.5); + EXPECT_FLOAT_EQ(bb->get("v"), 2.5f); + + // The string conversions must agree with std::from_chars, which + // reports result_out_of_range for these literals. + EXPECT_ANY_THROW((void)convertFromString("1e300")); + EXPECT_ANY_THROW((void)convertFromString("-1e300")); + EXPECT_FLOAT_EQ(convertFromString("1.5"), 1.5f); + EXPECT_ANY_THROW((void)BT::Any(std::string("1e300")).cast()); + EXPECT_FLOAT_EQ(BT::Any(std::string("1.5")).cast(), 1.5f); +} + TEST(BasicTypes, ConvertFromString_Bool) { ASSERT_TRUE(convertFromString("true")); From 1fbdad67c341c559188c232da343079165d1fc8c Mon Sep 17 00:00:00 2001 From: Davide Faconti Date: Sat, 24 Oct 2026 15:34:22 +0200 Subject: [PATCH 2/2] fix the float range check at the FLT_MAX boundary The check rejected every finite double above FLT_MAX, but values below FLT_MAX + half an ulp still round to FLT_MAX: 3.4028235e38, the usual printout of FLT_MAX, was accepted before and std::from_chars accepts it too. Reject only values from FLT_MAX + half an ulp on, which round to infinity, in both checkTruncation and the macOS convertFromString fallback. Also skip the check for widening conversions (float to double), where casting the limits of To to From would itself overflow, and move it to a helper to keep checkTruncation readable. Co-Authored-By: Claude Opus 5.5 --- .../behaviortree_cpp/utils/convert_impl.hpp | 34 +++++++++++++------ src/basic_types.cpp | 4 +-- tests/gtest_basic_types.cpp | 13 +++++++ 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/include/behaviortree_cpp/utils/convert_impl.hpp b/include/behaviortree_cpp/utils/convert_impl.hpp index af6616d3d..cb1d0a174 100644 --- a/include/behaviortree_cpp/utils/convert_impl.hpp +++ b/include/behaviortree_cpp/utils/convert_impl.hpp @@ -87,6 +87,29 @@ inline void checkLowerLimit(const From& from) } } +// Rounding to a narrower floating point type is accepted, but a finite value +// beyond the range of To has no representation there: the conversion is +// undefined and in practice produces an infinity. +template +inline void checkFloatingPointRange(const From& from) +{ + if constexpr(std::numeric_limits::max_exponent < + std::numeric_limits::max_exponent) + { + // Values below max + half an ulp still round to max (e.g. 3.4028235e38, + // the usual printout of FLT_MAX); from there on they round to infinity. + constexpr int half_ulp_exp = + std::numeric_limits::max_exponent - std::numeric_limits::digits - 1; + const From limit = static_cast(std::numeric_limits::max()) + + std::ldexp(From(1), half_ulp_exp); + if(std::isfinite(from) && std::fabs(from) >= limit) + { + throw std::runtime_error("Value outside the numeric limits of the floating point " + "type"); + } + } +} + template inline void checkTruncation(const From& from) { @@ -135,16 +158,7 @@ inline void checkTruncation(const From& from) // Handle floating point to floating point (e.g. double to float) else if constexpr(std::is_floating_point_v && std::is_floating_point_v) { - // Rounding to the narrower type is accepted, but a finite value beyond the - // range of To has no representation there: the conversion is undefined and - // in practice produces an infinity. - if(std::isfinite(from) && - (from > static_cast(std::numeric_limits::max()) || - from < static_cast(std::numeric_limits::lowest()))) - { - throw std::runtime_error("Value outside the numeric limits of the floating point " - "type"); - } + checkFloatingPointRange(from); } // Handle other conversions else diff --git a/src/basic_types.cpp b/src/basic_types.cpp index 172f3ebac..aa740f766 100644 --- a/src/basic_types.cpp +++ b/src/basic_types.cpp @@ -303,8 +303,8 @@ float convertFromString(StringView str) } // std::from_chars reports result_out_of_range for a finite value that // does not fit in a float; casting the double would silently give an infinity. - if(std::isfinite(result) && - std::fabs(result) > static_cast(std::numeric_limits::max())) + // 0x1.ffffffp+127 is FLT_MAX + half an ulp: values below it round to FLT_MAX. + if(std::isfinite(result) && std::fabs(result) >= 0x1.ffffffp+127) { throw RuntimeError(StrCat("Can't convert string [", str, "] to float")); } diff --git a/tests/gtest_basic_types.cpp b/tests/gtest_basic_types.cpp index af0450c3d..a224bc127 100644 --- a/tests/gtest_basic_types.cpp +++ b/tests/gtest_basic_types.cpp @@ -180,6 +180,19 @@ TEST(BasicTypes, FloatConversionLimits) EXPECT_FLOAT_EQ(BT::Any(3.4e38).cast(), 3.4e38f); EXPECT_FLOAT_EQ(BT::Any(-3.4e38).cast(), -3.4e38f); + // 3.4028235e38 (the usual printout of FLT_MAX) is a bit above FLT_MAX as a + // double, but below FLT_MAX + half an ulp, so it rounds to FLT_MAX. + constexpr float flt_max = std::numeric_limits::max(); + EXPECT_EQ(BT::Any(3.4028235e38).cast(), flt_max); + EXPECT_EQ(BT::Any(-3.4028235e38).cast(), -flt_max); + EXPECT_EQ(convertFromString("3.4028235e38"), flt_max); + // FLT_MAX + half an ulp rounds to infinity. + EXPECT_ANY_THROW((void)BT::Any(0x1.ffffffp+127).cast()); + EXPECT_ANY_THROW((void)convertFromString("6.8056473e38")); + // Widening never throws, and non-finite values pass through. + EXPECT_EQ(BT::Any(flt_max).cast(), double(flt_max)); + EXPECT_TRUE(std::isinf(BT::Any(std::numeric_limits::infinity()).cast())); + // Same path through a blackboard entry holding a double. auto bb = BT::Blackboard::create(); bb->set("v", 1e300);