diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index 0b8f155ac5..8685841e26 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -569,51 +569,126 @@ 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 + // Iteratively flatten nested containers to prevent + // stack overflow from recursive destruction. Children + // are promoted into the parent array's spare capacity + // (no allocation), with overflow to a heap stack. If + // the heap stack cannot be allocated, the catch block + // lets RAII recurse instead of calling std::terminate. + +#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) + try { - stack.reserve(object->size()); - for (auto&& it : *object) - { - stack.push_back(std::move(it.second)); - } - } - - 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(); +#endif + // Uses the default allocator, we must not be + // affected by a user allocator that throws or + // copies on move. + std::vector stack; - // if current_item is array/object, move - // its children to the stack to be processed later - if (current_item.is_array()) + 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(); + // Move children from source into parent, + // leaving `remain` behind. Stays within + // spare capacity. Uses resize() rather than + // erase() to avoid instantiating move- + // assignment in MSVC module builds. + auto* parent = array; + auto promote = [parent](basic_json & source, size_type remain) + { + if (source.is_array()) + { + auto* src = source.m_data.m_value.array; + auto first = src->begin() + static_cast(remain); + std::move(first, src->end(), std::back_inserter(*parent)); + src->resize(remain); + } + else if (source.is_object()) + { + auto* src = source.m_data.m_value.object; + const auto to_move = src->size() - remain; + for (size_type i = 0; i < to_move; ++i) + { + parent->push_back(std::move(src->begin()->second)); + src->erase(src->begin()); + } + } + }; + + while (!array->empty()) + { + if (array->back().is_structured()) + { + const auto spare = parent->capacity() - parent->size(); + const auto n = array->back().size(); + + if (n <= spare + 1) + { + // All fit (+1 from freeing container slot) + basic_json nested(std::move(array->back())); + array->pop_back(); + promote(nested, 0); + } + else if (spare > 0) + { + // Partial: revisited once promoted + // children are processed + promote(array->back(), n - spare); + } + else + { + // No capacity: overflow to stack + stack.push_back(std::move(array->back())); + array->pop_back(); + } + } + else + { + array->pop_back(); + } + } } - else if (current_item.is_object()) + else { - for (auto&& it : *current_item.m_data.m_value.object) + // Move structured values to the stack so that + // the object's destruction only encounters leaves. + for (auto& it : *object) { - stack.push_back(std::move(it.second)); + if (it.second.is_structured()) + { + 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()) + { + basic_json current(std::move(stack.back())); // NOLINT(misc-const-correctness) + stack.pop_back(); + + if (current.is_array()) + { + auto* src = current.m_data.m_value.array; + std::move(src->begin(), src->end(), std::back_inserter(stack)); + src->clear(); + } + else if (current.is_object()) + { + auto* src = current.m_data.m_value.object; + for (auto& it : *src) + { + stack.push_back(std::move(it.second)); + } + src->clear(); + } + } +#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) } + catch (...) // NOLINT(bugprone-empty-catch) + { + // Stack allocation failed, RAII cleans up; remaining + // elements are destroyed recursively below. + } +#endif } switch (t) diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index e2bb8517b6..bd609b887d 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -20806,51 +20806,126 @@ 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; + // Iteratively flatten nested containers to prevent + // stack overflow from recursive destruction. Children + // are promoted into the parent array's spare capacity + // (no allocation), with overflow to a heap stack. If + // the heap stack cannot be allocated, the catch block + // lets RAII recurse instead of calling std::terminate. - // move the top-level items to stack - if (t == value_t::array) +#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) + try { - 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()) - { - // move the last item to a local variable to be processed - basic_json current_item(std::move(stack.back())); - stack.pop_back(); +#endif + // Uses the default allocator, we must not be + // affected by a user allocator that throws or + // copies on move. + std::vector stack; - // if current_item is array/object, move - // its children to the stack to be processed later - if (current_item.is_array()) + 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)); + // Move children from source into parent, + // leaving `remain` behind. Stays within + // spare capacity. Uses resize() rather than + // erase() to avoid instantiating move- + // assignment in MSVC module builds. + auto* parent = array; + auto promote = [parent](basic_json & source, size_type remain) + { + if (source.is_array()) + { + auto* src = source.m_data.m_value.array; + auto first = src->begin() + static_cast(remain); + std::move(first, src->end(), std::back_inserter(*parent)); + src->resize(remain); + } + else if (source.is_object()) + { + auto* src = source.m_data.m_value.object; + const auto to_move = src->size() - remain; + for (size_type i = 0; i < to_move; ++i) + { + parent->push_back(std::move(src->begin()->second)); + src->erase(src->begin()); + } + } + }; + + while (!array->empty()) + { + if (array->back().is_structured()) + { + const auto spare = parent->capacity() - parent->size(); + const auto n = array->back().size(); - current_item.m_data.m_value.array->clear(); + if (n <= spare + 1) + { + // All fit (+1 from freeing container slot) + basic_json nested(std::move(array->back())); + array->pop_back(); + promote(nested, 0); + } + else if (spare > 0) + { + // Partial: revisited once promoted + // children are processed + promote(array->back(), n - spare); + } + else + { + // No capacity: overflow to stack + stack.push_back(std::move(array->back())); + array->pop_back(); + } + } + else + { + array->pop_back(); + } + } } - else if (current_item.is_object()) + else { - for (auto&& it : *current_item.m_data.m_value.object) + // Move structured values to the stack so that + // the object's destruction only encounters leaves. + for (auto& it : *object) { - stack.push_back(std::move(it.second)); + if (it.second.is_structured()) + { + 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()) + { + basic_json current(std::move(stack.back())); // NOLINT(misc-const-correctness) + stack.pop_back(); + + if (current.is_array()) + { + auto* src = current.m_data.m_value.array; + std::move(src->begin(), src->end(), std::back_inserter(stack)); + src->clear(); + } + else if (current.is_object()) + { + auto* src = current.m_data.m_value.object; + for (auto& it : *src) + { + stack.push_back(std::move(it.second)); + } + src->clear(); + } + } +#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) } + catch (...) // NOLINT(bugprone-empty-catch) + { + // Stack allocation failed, RAII cleans up; remaining + // elements are destroyed recursively below. + } +#endif } switch (t) diff --git a/tests/src/unit-allocator.cpp b/tests/src/unit-allocator.cpp index 6f2c70148c..951b773f2e 100644 --- a/tests/src/unit-allocator.cpp +++ b/tests/src/unit-allocator.cpp @@ -261,3 +261,95 @@ TEST_CASE("bad my_allocator::construct") j["test"].push_back("should not leak"); } } + +#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) +namespace +{ +struct QuotaReached : std::exception {}; + +template +struct allocator_controlled_throw : std::allocator +{ + static bool& should_throw() + { + static bool flag = false; + return flag; + } + + allocator_controlled_throw() = default; + template + allocator_controlled_throw(allocator_controlled_throw /*unused*/) {} + + template + struct rebind + { + using other = allocator_controlled_throw; + }; + + T* allocate(size_t n) + { + if (should_throw()) + { + throw QuotaReached{}; + } + return std::allocator::allocate(n); + } +}; +} // namespace + +TEST_CASE("~basic_json tolerates allocator exceptions") +{ + using my_alloc_json = nlohmann::basic_json; + + SECTION("flat array") + { + { + auto j = my_alloc_json{1, 2, 3, 4}; + allocator_controlled_throw::should_throw() = true; + } // should not std::terminate + allocator_controlled_throw::should_throw() = false; + } + + SECTION("nested array") + { + { + auto j = my_alloc_json::array(); + j.push_back(my_alloc_json{1, 2}); + j.push_back(my_alloc_json{3, 4}); + allocator_controlled_throw::should_throw() = true; + } + allocator_controlled_throw::should_throw() = false; + } + + SECTION("object with nested array") + { + { + auto j = my_alloc_json::object(); + j["arr"] = my_alloc_json{1, 2, 3}; + j["val"] = 42; + allocator_controlled_throw::should_throw() = true; + } + allocator_controlled_throw::should_throw() = false; + } + + SECTION("deeply nested array") + { + { + auto inner = my_alloc_json{1, 2}; + auto mid = my_alloc_json::array(); + mid.push_back(std::move(inner)); + auto outer = my_alloc_json::array(); + outer.push_back(std::move(mid)); + allocator_controlled_throw::should_throw() = true; + } + allocator_controlled_throw::should_throw() = false; + } +} +#endif