From af394fca9cd6a7a7a4cb157d1e19899237806be5 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Tue, 29 Sep 2026 16:40:54 +0200 Subject: [PATCH] Share one nesting depth limit between all bounded descents Copying and comparing stopped their descent at basic_json::nesting_depth_limit(), while serializing, hashing and merging used detail::recursion_depth_limit(). Both were 128, but nothing kept them equal. The thread-local count now tests against detail::recursion_depth_limit() as well, and a static_assert keeps the limit small enough for the byte that holds the count. Signed-off-by: Niels Lohmann --- .../nlohmann/detail/recursion_depth_limit.hpp | 8 +++++- include/nlohmann/json.hpp | 18 +++++-------- single_include/nlohmann/json.hpp | 26 ++++++++++--------- 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/include/nlohmann/detail/recursion_depth_limit.hpp b/include/nlohmann/detail/recursion_depth_limit.hpp index fe3bd8026..6553a4d05 100644 --- a/include/nlohmann/detail/recursion_depth_limit.hpp +++ b/include/nlohmann/detail/recursion_depth_limit.hpp @@ -19,11 +19,17 @@ namespace detail /*! @brief the number of nesting levels an operation recurses into -Operations that walk a value (serializing, hashing, merging, ...) recurse once +Operations that walk a value (copying, comparing, serializing, hashing, merging, +...) recurse once per nesting level, which is fastest, but a value nested deeply enough would exhaust the call stack. So they recurse only this many levels deep and finish whatever lies below with an explicit stack. All of them share this limit. +Most of them pass the depth down as an argument. The copy constructor and the +comparison operators cannot, as their signatures are fixed, so they count it +in basic_json::nesting_depth() instead, a byte per thread; the limit must +therefore stay below 255. + @sa https://github.com/nlohmann/json/issues/5387 */ constexpr std::size_t recursion_depth_limit() noexcept diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index 500fcddf2..a9cfcb520 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -898,12 +898,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec } #ifndef JSON_NO_THREAD_LOCAL - /// the number of levels an operation descends into before it finishes the - /// value below it without the call stack - static constexpr std::uint8_t nesting_depth_limit() - { - return 128; - } + // nesting_depth() is a byte and may exceed the limit by one level + static_assert(detail::recursion_depth_limit() < 255, "the nesting depth count must fit in a byte"); /*! @brief how many levels the operation going on in this thread has descended into @@ -945,7 +941,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec static_cast(may_descend); return true; #else - return !may_descend || nesting_depth() >= nesting_depth_limit(); + return !may_descend || nesting_depth() >= detail::recursion_depth_limit(); #endif } @@ -969,7 +965,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec #ifdef JSON_NO_THREAD_LOCAL : m_okay(false) #else - : m_okay(nesting_depth() < nesting_depth_limit()) + : m_okay(nesting_depth() < detail::recursion_depth_limit()) #endif { #ifndef JSON_NO_THREAD_LOCAL @@ -1173,7 +1169,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec The values whose copy has not been created yet are kept on an explicit worklist rather than on the call stack. This is only reached for values - nested deeper than @ref nesting_depth_limit levels, which is why it copies + nested deeper than @ref detail::recursion_depth_limit levels, which is why it copies every container by hand instead of letting the container do it: the fast ways of doing so would descend into the elements and defeat the purpose. */ @@ -1239,7 +1235,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec Copying a container copies its elements, so a value nested deeply enough used to exhaust the call stack. The descent is bounded here: the first - @ref nesting_depth_limit levels are copied by the containers themselves, just + @ref detail::recursion_depth_limit levels are copied by the containers themselves, just as they always were, and anything below that is copied without the call stack by @ref copy_iteratively. Copying a value can therefore no longer exhaust the stack, however deeply it is nested, just like destroying one @@ -1377,7 +1373,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 nesting_depth_limit levels, so + Reached once a comparison has descended @ref detail::recursion_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 diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 576498738..5e84da710 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -7281,11 +7281,17 @@ namespace detail /*! @brief the number of nesting levels an operation recurses into -Operations that walk a value (serializing, hashing, merging, ...) recurse once +Operations that walk a value (copying, comparing, serializing, hashing, merging, +...) recurse once per nesting level, which is fastest, but a value nested deeply enough would exhaust the call stack. So they recurse only this many levels deep and finish whatever lies below with an explicit stack. All of them share this limit. +Most of them pass the depth down as an argument. The copy constructor and the +comparison operators cannot, as their signatures are fixed, so they count it +in basic_json::nesting_depth() instead, a byte per thread; the limit must +therefore stay below 255. + @sa https://github.com/nlohmann/json/issues/5387 */ constexpr std::size_t recursion_depth_limit() noexcept @@ -26979,12 +26985,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec } #ifndef JSON_NO_THREAD_LOCAL - /// the number of levels an operation descends into before it finishes the - /// value below it without the call stack - static constexpr std::uint8_t nesting_depth_limit() - { - return 128; - } + // nesting_depth() is a byte and may exceed the limit by one level + static_assert(detail::recursion_depth_limit() < 255, "the nesting depth count must fit in a byte"); /*! @brief how many levels the operation going on in this thread has descended into @@ -27026,7 +27028,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec static_cast(may_descend); return true; #else - return !may_descend || nesting_depth() >= nesting_depth_limit(); + return !may_descend || nesting_depth() >= detail::recursion_depth_limit(); #endif } @@ -27050,7 +27052,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec #ifdef JSON_NO_THREAD_LOCAL : m_okay(false) #else - : m_okay(nesting_depth() < nesting_depth_limit()) + : m_okay(nesting_depth() < detail::recursion_depth_limit()) #endif { #ifndef JSON_NO_THREAD_LOCAL @@ -27254,7 +27256,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec The values whose copy has not been created yet are kept on an explicit worklist rather than on the call stack. This is only reached for values - nested deeper than @ref nesting_depth_limit levels, which is why it copies + nested deeper than @ref detail::recursion_depth_limit levels, which is why it copies every container by hand instead of letting the container do it: the fast ways of doing so would descend into the elements and defeat the purpose. */ @@ -27320,7 +27322,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec Copying a container copies its elements, so a value nested deeply enough used to exhaust the call stack. The descent is bounded here: the first - @ref nesting_depth_limit levels are copied by the containers themselves, just + @ref detail::recursion_depth_limit levels are copied by the containers themselves, just as they always were, and anything below that is copied without the call stack by @ref copy_iteratively. Copying a value can therefore no longer exhaust the stack, however deeply it is nested, just like destroying one @@ -27458,7 +27460,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 nesting_depth_limit levels, so + Reached once a comparison has descended @ref detail::recursion_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