Skip to content

Commit

Permalink
Fix ~basic_json causing std::terminate
Browse files Browse the repository at this point in the history
If there is an allocation problem in `destroy` fallback to recursion
approach.

Signed-off-by: Gareth Lloyd <[email protected]>
  • Loading branch information
Ignition committed Feb 17, 2025
1 parent 0b6881a commit 683c809
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 68 deletions.
78 changes: 44 additions & 34 deletions include/nlohmann/json.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -568,50 +568,60 @@ 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<basic_json> 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())
try
{
// move the last item to 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<basic_json, allocator_type> 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 get destructed
// since it doesn't have any children
while (!stack.empty())
{
// move the last item to 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 get destructed
// since it doesn't have any children
}
}
catch (...) // NOLINT(bugprone-empty-catch)
{
// Recursion avoidance has issue allocating temporary space. This may have been `std::bad_alloc`
// or any other exception thrown by a custom allocator.
// RAII will correctly clean up anything moved into `stack`.
// Then we continue with regular recursion based destroy, which will not heap allocate.
}
}

Expand Down
78 changes: 44 additions & 34 deletions single_include/nlohmann/json.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20567,50 +20567,60 @@ 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<basic_json> 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
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<basic_json, allocator_type> stack;

while (!stack.empty())
{
// move the last item to 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 get destructed
// since it doesn't have any children
while (!stack.empty())
{
// move the last item to 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 get destructed
// since it doesn't have any children
}
}
catch (...) // NOLINT(bugprone-empty-catch)
{
// Recursion avoidance has issue allocating temporary space. This may have been `std::bad_alloc`
// or any other exception thrown by a custom allocator.
// RAII will correctly clean up anything moved into `stack`.
// Then we continue with regular recursion based destroy, which will not heap allocate.
}
}

Expand Down
53 changes: 53 additions & 0 deletions tests/src/unit-allocator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -261,3 +261,56 @@ TEST_CASE("bad my_allocator::construct")
j["test"].push_back("should not leak");
}
}

namespace
{
thread_local bool should_throw = false;

struct QuotaReached : std::exception {};

template<class T>
struct allocator_controlled_throw : std::allocator<T>
{
allocator_controlled_throw() = default;
template <class U>
allocator_controlled_throw(allocator_controlled_throw<U> /*unused*/) {}

template <class U>
struct rebind
{
using other = allocator_controlled_throw<U>;
};

T* allocate(size_t n)
{
if (should_throw)
{
throw QuotaReached{};
}
return std::allocator<T>::allocate(n);
}
};
} // namespace

TEST_CASE("controlled my_allocator::allocate")
{
SECTION("~basic_json tolerant of internal exceptions")
{
using my_alloc_json = nlohmann::basic_json<std::map,
std::vector,
std::string,
bool,
std::int64_t,
std::uint64_t,
double,
allocator_controlled_throw>;

{
auto j = my_alloc_json{1, 2, 3, 4};
should_throw = true;
// `j` is destroyed, ~basic_json is noexcept
// if allocation attempted, exception thrown
// exception should be internally handled
} // should not std::terminate
}
}

0 comments on commit 683c809

Please sign in to comment.