From 0bc01a066cc1bb87705072eed6b195b08d53525a Mon Sep 17 00:00:00 2001 From: Jonas Greitemann Date: Sat, 6 Jun 2026 16:40:58 +0200 Subject: [PATCH] Add (failing) regression test for `strtod` locale TOCTOU bug In debug builds, this test fails with `SIGABRT` due to an assertion. In release builds, truncation of the fractional part will be detected. Sometimes, this test may also cause memory violation (`SIGSEGV`) due to it being undefined behavior to set locale and use locale-dependent functions concurrently. On Windows, this does not happen since the global locale is protected by a mutex. This test assumes that a locale called "de_DE.UTF-8" exists on the host. If it does not, the test bails out but does not fail. The locale name is hard-coded because it is impossible to query available locales portably. "de_DE.UTF-8" is an accepted format on Ubuntu, macOS, and Windows. Signed-off-by: Jonas Greitemann --- tests/CMakeLists.txt | 4 ++++ tests/src/unit-regression2.cpp | 41 ++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4383b582c..47524982c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -121,6 +121,10 @@ json_test_set_test_options(test-disabled_exceptions # raise timeout of expensive Unicode test json_test_set_test_options(test-unicode4 TEST_PROPERTIES TIMEOUT 3000) +# link pthreads to tests that need it +find_package(Threads REQUIRED) +json_test_set_test_options(test-regression2 LINK_LIBRARIES Threads::Threads) + ############################################################################# # add unit tests ############################################################################# diff --git a/tests/src/unit-regression2.cpp b/tests/src/unit-regression2.cpp index 7ae4170d3..83b646475 100644 --- a/tests/src/unit-regression2.cpp +++ b/tests/src/unit-regression2.cpp @@ -26,8 +26,11 @@ using ordered_json = nlohmann::ordered_json; using namespace nlohmann::literals; // NOLINT(google-build-using-namespace) #endif +#include +#include #include #include +#include #include #include @@ -1239,6 +1242,44 @@ TEST_CASE("regression tests 2") CHECK(j == json({2, 4, 6})); } #endif + + SECTION("issue #5198 - TOCTOU race between lexer construction and locale changes causes float truncation") + { + std::atomic stop_requested{}; + std::thread switcher([&stop_requested] + { + bool german = true; + const std::string initial_locale = std::setlocale(LC_NUMERIC, nullptr); + + while (!stop_requested) + { + const bool setlocale_res = std::setlocale(LC_NUMERIC, german ? "de_DE.UTF-8" : "C"); + WARN_MESSAGE(setlocale_res, + "Setting locale failed, probably because de_DE.UTF-8 is not installed. " + "This test was skipped as it would be inconclusive."); + if (!setlocale_res) + { + stop_requested = true; + } + german = !german; + } + + (void)std::setlocale(LC_NUMERIC, initial_locale.c_str()); // restore original locale + }); + + for (std::size_t i = 0; i < 10000 && !stop_requested; ++i) + { + const json j = json::parse("{\"val\": 99.123456789}"); + const double parsed = j.value("val", 0.0); + if (!CHECK(parsed == 99.123456789)) + { + break; + } + } + + stop_requested = true; + switcher.join(); + } } TEST_CASE_TEMPLATE("issue #4798 - nlohmann::json::to_msgpack() encode float NaN as double", T, double, float) // NOLINT(readability-math-missing-parentheses, bugprone-throwing-static-initialization)