Use the shared descent bookkeeping rather than a second set

Comparing kept a thread_local count, a limit and a guard of its own beside
the ones copying already had, all three the same thing under a different
name. They are gone; the shared nesting_depth_exhausted() and
nesting_depth_guard do the work.

Also say in compare_leaves what happens to a pair that is an array on one
side and an object on the other, since the answer is not obvious from the
code: an operator only descends into two values of the same type, so such
a pair is told apart by its types alone - unequal, and ordered the way the
types are - exactly as it is above the bound.

And record what the explicit stack costs: the comparison operators are
noexcept and the container comparison this replaces allocated nothing, so
running out of memory here ends the process instead of throwing. It takes
a value nested past the bound and an exhausted heap to reach, and the same
comparison used to exhaust the call stack, but it is a new way to fail.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann
2026-08-21 08:56:43 +02:00
parent be8fc0a929
commit 835a5e6c3a
2 changed files with 38 additions and 132 deletions
+19 -66
View File
@@ -846,13 +846,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
static thread_local std::size_t depth = 0; // NOLINT(misc-use-internal-linkage)
return depth;
}
/// @brief how many levels the comparison going on in this thread has descended into
static std::size_t& compare_depth() noexcept
{
static thread_local std::size_t depth = 0; // NOLINT(misc-use-internal-linkage)
return depth;
}
#endif
/*!
@@ -1205,53 +1198,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
/// ordered at all, such as a discarded value or a NaN
enum class compare_result { less, equal, greater, unordered };
/*!
@brief counts one level of a comparison for as long as it runs
Does nothing without thread_local storage, where no descent is made at all.
*/
class compare_depth_guard
{
public:
compare_depth_guard() noexcept
{
#ifndef JSON_NO_THREAD_LOCAL
++compare_depth();
#endif
}
~compare_depth_guard() noexcept
{
#ifndef JSON_NO_THREAD_LOCAL
--compare_depth();
#endif
}
compare_depth_guard(const compare_depth_guard&) = delete;
compare_depth_guard& operator=(const compare_depth_guard&) = delete;
compare_depth_guard(compare_depth_guard&&) = delete;
compare_depth_guard& operator=(compare_depth_guard&&) = delete;
};
/*!
@brief whether a comparison must stop descending and finish iteratively
@a may_descend says whether the operator descends at all; it is constant at
every call site, and is passed rather than tested by the caller so that the
test does not become a constant condition there, which MSVC reports (C4127).
*/
static bool compare_descent_exhausted(bool may_descend) noexcept
{
#ifdef JSON_NO_THREAD_LOCAL
// without a counter of its own per thread, the descent cannot be
// bounded without racing another one, so none is made
static_cast<void>(may_descend);
return true;
#else
return !may_descend || compare_depth() >= compare_depth_limit();
#endif
}
#if JSON_HAS_THREE_WAY_COMPARISON
/// @brief the ordering that @a result stands for
static std::partial_ordering to_partial_ordering(compare_result result) noexcept // *NOPAD*
@@ -1271,18 +1217,18 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
}
#endif
/// the number of levels a comparison descends into before it compares what
/// is left without the call stack
static constexpr std::size_t compare_depth_limit()
{
return 128;
}
/*!
@brief compare two values that are not both an array or both an object
Such a pair is compared by the operators themselves, which cannot descend
into it and therefore cannot recurse.
That holds for a pair whose types differ as much as for a pair of leaves: an
array and an object are told apart by their types alone, because an operator
only ever descends into two values of the same type. So `==` reports them as
unequal without looking inside either, and an ordering falls back to the
order of the types - an object sorts before an array - exactly as it does
for a value that is not nested deeply enough to get here.
*/
template<bool Ordered>
static compare_result compare_leaves(const_reference lhs, const_reference rhs) noexcept
@@ -1356,7 +1302,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
/*!
@brief compare @a lhs and @a rhs without descending into them
Reached once a comparison has descended @ref compare_depth_limit levels, so
Reached once a comparison has descended @ref nesting_depth_limit levels, so
that comparing values cannot exhaust the call stack however deeply they are
nested. The two values are walked in lockstep on an explicit stack and
compared lexicographically, element by element in the order the containers
@@ -1365,6 +1311,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
nlohmann::ordered_map in insertion order. An object type that enumerates its
entries in an unspecified order, such as std::unordered_map, compares them
pairwise instead; the difference could only ever show below the bound.
Note that the stack this walks with is allocated, while the comparison
operators are noexcept and the container comparison this replaces allocated
nothing. Failing that allocation therefore ends the process rather than
throwing. It only arises for values nested past the bound, and only when
memory has run out - where the same comparison used to exhaust the call
stack instead - but it is a way to fail that the operators did not have.
*/
template<bool Ordered>
static compare_result compare_iteratively(const_reference lhs, const_reference rhs,
@@ -4290,21 +4243,21 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec
{ \
case value_t::array: \
{ \
if (JSON_HEDLEY_UNLIKELY(compare_descent_exhausted(may_descend))) \
if (JSON_HEDLEY_UNLIKELY(nesting_depth_exhausted(may_descend))) \
{ \
return (deep_result); \
} \
const compare_depth_guard guard; \
const nesting_depth_guard guard; \
return (*lhs.m_data.m_value.array) op (*rhs.m_data.m_value.array); \
} \
\
case value_t::object: \
{ \
if (JSON_HEDLEY_UNLIKELY(compare_descent_exhausted(may_descend))) \
if (JSON_HEDLEY_UNLIKELY(nesting_depth_exhausted(may_descend))) \
{ \
return (deep_result); \
} \
const compare_depth_guard guard; \
const nesting_depth_guard guard; \
return (*lhs.m_data.m_value.object) op (*rhs.m_data.m_value.object); \
} \
\