From d428f791b64e162955d9b448f1c28b5db997ac9c Mon Sep 17 00:00:00 2001 From: Michael Sam Date: Mon, 6 Jul 2026 13:39:03 +0300 Subject: [PATCH] Fix allocation failure during JSON destruction Signed-off-by: Michael Sam --- include/nlohmann/json.hpp | 76 ++++++++++++++++++-------------- single_include/nlohmann/json.hpp | 76 ++++++++++++++++++-------------- tests/src/unit-regression2.cpp | 60 +++++++++++++++++++++++++ 3 files changed, 144 insertions(+), 68 deletions(-) diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index e3bd3ea0bf..2d72ea45f6 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -598,50 +598,58 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec } if (t == value_t::array || t == value_t::object) { - // flatten the current json_value to a heap-allocated stack - std::vector stack; - - // move the top-level items to stack - if (t == value_t::array) - { - stack.reserve(array->size()); - std::move(array->begin(), array->end(), std::back_inserter(stack)); - } - else - { - stack.reserve(object->size()); - for (auto&& it : *object) - { - stack.push_back(std::move(it.second)); - } - } - - while (!stack.empty()) + JSON_TRY { - // move the last item to a local variable to be processed - basic_json current_item(std::move(stack.back())); - stack.pop_back(); + // flatten the current json_value to a heap-allocated stack + std::vector stack; - // if current_item is array/object, move - // its children to the stack to be processed later - if (current_item.is_array()) + // move the top-level items to stack + if (t == value_t::array) { - std::move(current_item.m_data.m_value.array->begin(), current_item.m_data.m_value.array->end(), std::back_inserter(stack)); - - current_item.m_data.m_value.array->clear(); + stack.reserve(array->size()); + std::move(array->begin(), array->end(), std::back_inserter(stack)); } - else if (current_item.is_object()) + else { - for (auto&& it : *current_item.m_data.m_value.object) + stack.reserve(object->size()); + for (auto&& it : *object) { stack.push_back(std::move(it.second)); } - - current_item.m_data.m_value.object->clear(); } - // it's now safe that current_item gets destructed - // since it doesn't have any children + while (!stack.empty()) + { + // move the last item to a local variable to be processed + basic_json current_item(std::move(stack.back())); + stack.pop_back(); + + // if current_item is array/object, move + // its children to the stack to be processed later + if (current_item.is_array()) + { + std::move(current_item.m_data.m_value.array->begin(), current_item.m_data.m_value.array->end(), std::back_inserter(stack)); + + current_item.m_data.m_value.array->clear(); + } + else if (current_item.is_object()) + { + for (auto&& it : *current_item.m_data.m_value.object) + { + stack.push_back(std::move(it.second)); + } + + current_item.m_data.m_value.object->clear(); + } + + // it's now safe that current_item gets destructed + // since it doesn't have any children + } + } + JSON_CATCH(...) // LCOV_EXCL_LINE + { + // If the heap-allocated stack cannot grow, fall back to + // recursive destruction rather than escaping a noexcept dtor. } } diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index cf7cb31c30..0be0007073 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -21424,50 +21424,58 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec } if (t == value_t::array || t == value_t::object) { - // flatten the current json_value to a heap-allocated stack - std::vector stack; - - // move the top-level items to stack - if (t == value_t::array) - { - stack.reserve(array->size()); - std::move(array->begin(), array->end(), std::back_inserter(stack)); - } - else + JSON_TRY { - stack.reserve(object->size()); - for (auto&& it : *object) - { - stack.push_back(std::move(it.second)); - } - } + // flatten the current json_value to a heap-allocated stack + std::vector stack; - while (!stack.empty()) - { - // move the last item to a local variable to be processed - basic_json current_item(std::move(stack.back())); - stack.pop_back(); - - // if current_item is array/object, move - // its children to the stack to be processed later - if (current_item.is_array()) + // move the top-level items to stack + if (t == value_t::array) { - std::move(current_item.m_data.m_value.array->begin(), current_item.m_data.m_value.array->end(), std::back_inserter(stack)); - - current_item.m_data.m_value.array->clear(); + stack.reserve(array->size()); + std::move(array->begin(), array->end(), std::back_inserter(stack)); } - else if (current_item.is_object()) + else { - for (auto&& it : *current_item.m_data.m_value.object) + stack.reserve(object->size()); + for (auto&& it : *object) { stack.push_back(std::move(it.second)); } - - current_item.m_data.m_value.object->clear(); } - // it's now safe that current_item gets destructed - // since it doesn't have any children + while (!stack.empty()) + { + // move the last item to a local variable to be processed + basic_json current_item(std::move(stack.back())); + stack.pop_back(); + + // if current_item is array/object, move + // its children to the stack to be processed later + if (current_item.is_array()) + { + std::move(current_item.m_data.m_value.array->begin(), current_item.m_data.m_value.array->end(), std::back_inserter(stack)); + + current_item.m_data.m_value.array->clear(); + } + else if (current_item.is_object()) + { + for (auto&& it : *current_item.m_data.m_value.object) + { + stack.push_back(std::move(it.second)); + } + + current_item.m_data.m_value.object->clear(); + } + + // it's now safe that current_item gets destructed + // since it doesn't have any children + } + } + JSON_CATCH(...) // LCOV_EXCL_LINE + { + // If the heap-allocated stack cannot grow, fall back to + // recursive destruction rather than escaping a noexcept dtor. } } diff --git a/tests/src/unit-regression2.cpp b/tests/src/unit-regression2.cpp index 3be8f9d5cf..f3042ed84f 100644 --- a/tests/src/unit-regression2.cpp +++ b/tests/src/unit-regression2.cpp @@ -27,7 +27,9 @@ using ordered_json = nlohmann::ordered_json; #endif #include +#include #include +#include #include #include @@ -91,6 +93,49 @@ DOCTEST_CLANG_SUPPRESS_WARNING("-Wexit-time-destructors") using float_json = nlohmann::basic_json; +#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(JSON_NOEXCEPTION) +namespace +{ +bool fail_next_global_allocation = false; + +void* checked_malloc(std::size_t size) +{ + if (fail_next_global_allocation) + { + fail_next_global_allocation = false; + throw std::bad_alloc(); + } + + if (void* const result = std::malloc(size)) + { + return result; + } + + throw std::bad_alloc(); +} +} // namespace + +void* operator new (std::size_t size) +{ + return checked_malloc(size); +} + +void* operator new[](std::size_t size) +{ + return checked_malloc(size); +} + +void operator delete (void* ptr) noexcept +{ + std::free(ptr); +} + +void operator delete[](void* ptr) noexcept +{ + std::free(ptr); +} +#endif + ///////////////////////////////////////////////////////////////////// // for #1647 ///////////////////////////////////////////////////////////////////// @@ -1454,4 +1499,19 @@ TEST_CASE("issue #4320 - custom base class must not leak nlohmann::detail into A CHECK(j == json({{"x", 1.0}, {"y", 2.0}, {"z", 3.0}})); } +#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(JSON_NOEXCEPTION) +TEST_CASE("regression test #5135 - destructor tolerates stack allocation failure") +{ + { + json j = json::array({json::array({1, 2}), json::object({{"key", json::array({3})}})}); + fail_next_global_allocation = true; + } + + const bool allocation_failure_was_injected = !fail_next_global_allocation; + fail_next_global_allocation = false; + + CHECK(allocation_failure_was_injected); +} +#endif + DOCTEST_CLANG_SUPPRESS_WARNING_POP